@silvery/examples 0.5.1 → 0.5.3

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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/{examples/apps → apps}/aichat/index.tsx +4 -3
  3. package/{examples/apps → apps}/async-data.tsx +4 -4
  4. package/apps/components.tsx +658 -0
  5. package/{examples/apps → apps}/data-explorer.tsx +8 -8
  6. package/{examples/apps → apps}/dev-tools.tsx +35 -19
  7. package/{examples/apps → apps}/inline-bench.tsx +3 -1
  8. package/{examples/apps → apps}/kanban.tsx +20 -22
  9. package/{examples/apps → apps}/layout-ref.tsx +6 -6
  10. package/{examples/apps → apps}/panes/index.tsx +1 -1
  11. package/{examples/apps → apps}/paste-demo.tsx +2 -2
  12. package/{examples/apps → apps}/scroll.tsx +2 -2
  13. package/{examples/apps → apps}/search-filter.tsx +1 -1
  14. package/apps/selection.tsx +342 -0
  15. package/apps/spatial-focus-demo.tsx +368 -0
  16. package/{examples/apps → apps}/task-list.tsx +1 -1
  17. package/apps/terminal-caps-demo.tsx +334 -0
  18. package/apps/text-selection-demo.tsx +189 -0
  19. package/apps/textarea.tsx +155 -0
  20. package/{examples/apps → apps}/theme.tsx +1 -1
  21. package/apps/vterm-demo/index.tsx +216 -0
  22. package/dist/cli.d.mts +1 -0
  23. package/dist/cli.mjs +190 -0
  24. package/dist/cli.mjs.map +1 -0
  25. package/layout/dashboard.tsx +953 -0
  26. package/layout/live-resize.tsx +282 -0
  27. package/layout/overflow.tsx +51 -0
  28. package/layout/text-layout.tsx +283 -0
  29. package/package.json +31 -11
  30. package/bin/cli.ts +0 -293
  31. package/examples/apps/components.tsx +0 -463
  32. package/examples/apps/textarea.tsx +0 -91
  33. /package/{examples/_banner.tsx → _banner.tsx} +0 -0
  34. /package/{examples/apps → apps}/aichat/components.tsx +0 -0
  35. /package/{examples/apps → apps}/aichat/script.ts +0 -0
  36. /package/{examples/apps → apps}/aichat/state.ts +0 -0
  37. /package/{examples/apps → apps}/aichat/types.ts +0 -0
  38. /package/{examples/apps → apps}/app-todo.tsx +0 -0
  39. /package/{examples/apps → apps}/cli-wizard.tsx +0 -0
  40. /package/{examples/apps → apps}/clipboard.tsx +0 -0
  41. /package/{examples/apps → apps}/explorer.tsx +0 -0
  42. /package/{examples/apps → apps}/gallery.tsx +0 -0
  43. /package/{examples/apps → apps}/outline.tsx +0 -0
  44. /package/{examples/apps → apps}/terminal.tsx +0 -0
  45. /package/{examples/apps → apps}/transform.tsx +0 -0
  46. /package/{examples/apps → apps}/virtual-10k.tsx +0 -0
  47. /package/{examples/components → components}/counter.tsx +0 -0
  48. /package/{examples/components → components}/hello.tsx +0 -0
  49. /package/{examples/components → components}/progress-bar.tsx +0 -0
  50. /package/{examples/components → components}/select-list.tsx +0 -0
  51. /package/{examples/components → components}/spinner.tsx +0 -0
  52. /package/{examples/components → components}/text-input.tsx +0 -0
  53. /package/{examples/components → components}/virtual-list.tsx +0 -0
@@ -0,0 +1,368 @@
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
+ if (import.meta.main) {
366
+ using handle = await run(<SpatialFocusBoard />, { mode: "fullscreen" })
367
+ await handle.waitUntilExit()
368
+ }
@@ -101,7 +101,7 @@ function TaskItem({ task, isSelected, isExpanded }: { task: Task; isSelected: bo
101
101
  <Box flexDirection="column">
102
102
  <Box>
103
103
  {isSelected ? (
104
- <Text backgroundColor="$primary" color="black">
104
+ <Text backgroundColor="$primary" color="$primary-fg">
105
105
  {" "}
106
106
  {checkbox} {task.title}{" "}
107
107
  </Text>