@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.
Files changed (37) hide show
  1. package/bin/cli.ts +286 -0
  2. package/examples/apps/aichat/components.tsx +469 -0
  3. package/examples/apps/aichat/index.tsx +207 -0
  4. package/examples/apps/aichat/script.ts +460 -0
  5. package/examples/apps/aichat/state.ts +326 -0
  6. package/examples/apps/aichat/types.ts +19 -0
  7. package/examples/apps/app-todo.tsx +201 -0
  8. package/examples/apps/async-data.tsx +208 -0
  9. package/examples/apps/cli-wizard.tsx +332 -0
  10. package/examples/apps/clipboard.tsx +183 -0
  11. package/examples/apps/components.tsx +463 -0
  12. package/examples/apps/data-explorer.tsx +490 -0
  13. package/examples/apps/dev-tools.tsx +379 -0
  14. package/examples/apps/explorer.tsx +731 -0
  15. package/examples/apps/gallery.tsx +653 -0
  16. package/examples/apps/inline-bench.tsx +136 -0
  17. package/examples/apps/kanban.tsx +267 -0
  18. package/examples/apps/layout-ref.tsx +185 -0
  19. package/examples/apps/outline.tsx +171 -0
  20. package/examples/apps/panes/index.tsx +205 -0
  21. package/examples/apps/paste-demo.tsx +198 -0
  22. package/examples/apps/scroll.tsx +77 -0
  23. package/examples/apps/search-filter.tsx +240 -0
  24. package/examples/apps/task-list.tsx +271 -0
  25. package/examples/apps/terminal.tsx +800 -0
  26. package/examples/apps/textarea.tsx +103 -0
  27. package/examples/apps/theme.tsx +515 -0
  28. package/examples/apps/transform.tsx +242 -0
  29. package/examples/apps/virtual-10k.tsx +405 -0
  30. package/examples/components/counter.tsx +45 -0
  31. package/examples/components/hello.tsx +34 -0
  32. package/examples/components/progress-bar.tsx +48 -0
  33. package/examples/components/select-list.tsx +50 -0
  34. package/examples/components/spinner.tsx +40 -0
  35. package/examples/components/text-input.tsx +57 -0
  36. package/examples/components/virtual-list.tsx +52 -0
  37. package/package.json +27 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Inline incremental rendering benchmark
3
+ *
4
+ * Measures output phase bytes and timing for inline mode:
5
+ * - Full render (bare outputPhase — always full render, no instance state)
6
+ * - Incremental render (createOutputPhase — instance-scoped cursor tracking)
7
+ *
8
+ * Usage:
9
+ * bun examples/apps/inline-bench.tsx
10
+ */
11
+
12
+ import { TerminalBuffer } from "@silvery/ag-term/buffer"
13
+ import { createOutputPhase, outputPhase } from "@silvery/ag-term/pipeline/output-phase"
14
+
15
+ const RUNS = 500
16
+
17
+ function fillBuffer(buf: TerminalBuffer, rows: number, prefix = ""): void {
18
+ for (let y = 0; y < rows; y++) {
19
+ const text = `${prefix}Item ${y}: Content line with some styling and longer text here`
20
+ for (let x = 0; x < Math.min(text.length, buf.width); x++) {
21
+ buf.setCell(x, y, { char: text[x]! })
22
+ }
23
+ }
24
+ }
25
+
26
+ interface BenchResult {
27
+ name: string
28
+ timings: number[]
29
+ bytes: number[]
30
+ }
31
+
32
+ function benchmarkOutputPhase(
33
+ name: string,
34
+ width: number,
35
+ height: number,
36
+ contentRows: number,
37
+ changedCells: number,
38
+ forceFullRender: boolean,
39
+ ): BenchResult {
40
+ const timings: number[] = []
41
+ const bytes: number[] = []
42
+
43
+ for (let i = 0; i < RUNS; i++) {
44
+ // Create fresh buffers each iteration
45
+ const prev = new TerminalBuffer(width, height)
46
+ fillBuffer(prev, contentRows)
47
+
48
+ const next = new TerminalBuffer(width, height)
49
+ fillBuffer(next, contentRows)
50
+
51
+ // Apply changes
52
+ for (let c = 0; c < changedCells; c++) {
53
+ const row = Math.floor((c / Math.max(changedCells, 1)) * contentRows)
54
+ const col = c % Math.min(10, width)
55
+ next.setCell(col, row, { char: "X" })
56
+ }
57
+
58
+ if (forceFullRender) {
59
+ // Bare outputPhase always uses fresh state → full render
60
+ outputPhase(null, prev, "inline", 0, height)
61
+ const t0 = performance.now()
62
+ const output = outputPhase(prev, next, "inline", 0, height)
63
+ const t1 = performance.now()
64
+ timings.push(t1 - t0)
65
+ bytes.push(output.length)
66
+ } else {
67
+ // createOutputPhase captures instance state → incremental after first render
68
+ const render = createOutputPhase({})
69
+ render(null, prev, "inline", 0, height) // first render (inits tracking)
70
+ const t0 = performance.now()
71
+ const output = render(prev, next, "inline", 0, height) // incremental
72
+ const t1 = performance.now()
73
+ timings.push(t1 - t0)
74
+ bytes.push(output.length)
75
+ }
76
+ }
77
+
78
+ timings.sort((a, b) => a - b)
79
+ bytes.sort((a, b) => a - b)
80
+
81
+ return { name, timings, bytes }
82
+ }
83
+
84
+ function printResult(r: BenchResult): void {
85
+ const p50t = r.timings[Math.floor(r.timings.length * 0.5)]!
86
+ const p95t = r.timings[Math.floor(r.timings.length * 0.95)]!
87
+ const p50b = r.bytes[Math.floor(r.bytes.length * 0.5)]!
88
+ const avgB = r.bytes.reduce((a, b) => a + b, 0) / r.bytes.length
89
+
90
+ console.log(
91
+ ` ${r.name.padEnd(40)} ` +
92
+ `p50=${p50t.toFixed(3).padStart(7)}ms ` +
93
+ `p95=${p95t.toFixed(3).padStart(7)}ms ` +
94
+ `bytes=${Math.round(p50b).toString().padStart(6)} (avg ${Math.round(avgB)})`,
95
+ )
96
+ }
97
+
98
+ function printComparison(full: BenchResult, incr: BenchResult): void {
99
+ const fullP50b = full.bytes[Math.floor(full.bytes.length * 0.5)]!
100
+ const incrP50b = incr.bytes[Math.floor(incr.bytes.length * 0.5)]!
101
+ const fullP50t = full.timings[Math.floor(full.timings.length * 0.5)]!
102
+ const incrP50t = incr.timings[Math.floor(incr.timings.length * 0.5)]!
103
+
104
+ const byteRatio = fullP50b / Math.max(incrP50b, 1)
105
+ const timeRatio = fullP50t / Math.max(incrP50t, 0.001)
106
+
107
+ console.log(
108
+ ` ${"→ savings".padEnd(40)} ` +
109
+ `time=${timeRatio.toFixed(1)}x faster ` +
110
+ `bytes=${byteRatio.toFixed(0)}x fewer (${fullP50b} → ${incrP50b})`,
111
+ )
112
+ }
113
+
114
+ async function main() {
115
+ console.log(`\n═══ Inline Output Phase: Full vs Incremental (${RUNS} runs) ═══\n`)
116
+
117
+ const configs = [
118
+ { label: "10 rows, 1 change", w: 80, h: 20, rows: 10, changes: 1 },
119
+ { label: "30 rows, 1 change", w: 120, h: 40, rows: 30, changes: 1 },
120
+ { label: "50 rows, 1 change", w: 120, h: 60, rows: 50, changes: 1 },
121
+ { label: "50 rows, 3 changes", w: 120, h: 60, rows: 50, changes: 3 },
122
+ { label: "50 rows, 10 changes", w: 120, h: 60, rows: 50, changes: 10 },
123
+ ]
124
+
125
+ for (const cfg of configs) {
126
+ console.log(`--- ${cfg.label} ---`)
127
+ const full = benchmarkOutputPhase(`full render`, cfg.w, cfg.h, cfg.rows, cfg.changes, true)
128
+ const incr = benchmarkOutputPhase(`incremental`, cfg.w, cfg.h, cfg.rows, cfg.changes, false)
129
+ printResult(full)
130
+ printResult(incr)
131
+ printComparison(full, incr)
132
+ console.log()
133
+ }
134
+ }
135
+
136
+ main().catch(console.error)
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Kanban Board Example
3
+ *
4
+ * A 3-column kanban board demonstrating:
5
+ * - Todo, In Progress, Done columns
6
+ * - Move items between columns with arrow keys
7
+ * - Each column uses native overflow="scroll" for scrolling
8
+ * - Flexbox layout for proportional sizing
9
+ */
10
+
11
+ import React, { useState } 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: "Kanban Board",
17
+ description: "3-column kanban with card movement and independent scroll",
18
+ demo: true,
19
+ features: ["Box flexDirection", "useInput", "backgroundColor", "multi-column layout"],
20
+ }
21
+
22
+ // ============================================================================
23
+ // Types
24
+ // ============================================================================
25
+
26
+ type ColumnId = "todo" | "inProgress" | "done"
27
+
28
+ interface Card {
29
+ id: number
30
+ title: string
31
+ tags: string[]
32
+ }
33
+
34
+ interface Column {
35
+ id: ColumnId
36
+ title: string
37
+ cards: Card[]
38
+ }
39
+
40
+ // ============================================================================
41
+ // Initial Data
42
+ // ============================================================================
43
+
44
+ const initialColumns: Column[] = [
45
+ {
46
+ id: "todo",
47
+ title: "To Do",
48
+ cards: [
49
+ { id: 1, title: "Design new landing page", tags: ["design"] },
50
+ { id: 2, title: "Write API documentation", tags: ["docs"] },
51
+ { id: 3, title: "Set up monitoring", tags: ["devops"] },
52
+ { id: 4, title: "Create onboarding flow", tags: ["ux"] },
53
+ { id: 5, title: "Database optimization", tags: ["backend"] },
54
+ { id: 6, title: "Mobile responsive fixes", tags: ["frontend"] },
55
+ { id: 7, title: "Add dark mode", tags: ["frontend", "ux"] },
56
+ { id: 8, title: "Implement caching", tags: ["backend"] },
57
+ ],
58
+ },
59
+ {
60
+ id: "inProgress",
61
+ title: "In Progress",
62
+ cards: [
63
+ { id: 9, title: "User authentication", tags: ["backend", "security"] },
64
+ { id: 10, title: "Dashboard redesign", tags: ["frontend", "design"] },
65
+ { id: 11, title: "API rate limiting", tags: ["backend"] },
66
+ ],
67
+ },
68
+ {
69
+ id: "done",
70
+ title: "Done",
71
+ cards: [
72
+ { id: 12, title: "Project setup", tags: ["devops"] },
73
+ { id: 13, title: "CI/CD pipeline", tags: ["devops"] },
74
+ { id: 14, title: "Initial wireframes", tags: ["design"] },
75
+ { id: 15, title: "Database schema", tags: ["backend"] },
76
+ ],
77
+ },
78
+ ]
79
+
80
+ // ============================================================================
81
+ // Components
82
+ // ============================================================================
83
+
84
+ const tagColors: Record<string, string> = {
85
+ frontend: "$info",
86
+ backend: "$accent",
87
+ design: "$warning",
88
+ devops: "$success",
89
+ docs: "$primary",
90
+ ux: "$muted",
91
+ security: "$error",
92
+ }
93
+
94
+ function Tag({ name }: { name: string }) {
95
+ const color = tagColors[name] ?? "$muted"
96
+ return (
97
+ <Text color={color} dim>
98
+ #{name}
99
+ </Text>
100
+ )
101
+ }
102
+
103
+ function CardComponent({ card, isSelected }: { card: Card; isSelected: boolean }) {
104
+ return (
105
+ <Box flexDirection="column" borderStyle="round" borderColor={isSelected ? "$primary" : "$border"} paddingX={1}>
106
+ {isSelected ? (
107
+ <Text backgroundColor="$primary" color="black" bold>
108
+ {card.title}
109
+ </Text>
110
+ ) : (
111
+ <Text>{card.title}</Text>
112
+ )}
113
+ <Box gap={1}>
114
+ {card.tags.map((tag) => (
115
+ <Tag key={tag} name={tag} />
116
+ ))}
117
+ </Box>
118
+ </Box>
119
+ )
120
+ }
121
+
122
+ function ColumnComponent({
123
+ column,
124
+ isSelected,
125
+ selectedCardIndex,
126
+ }: {
127
+ column: Column
128
+ isSelected: boolean
129
+ selectedCardIndex: number
130
+ }) {
131
+ return (
132
+ <Box flexDirection="column" flexGrow={1} borderStyle="single" borderColor={isSelected ? "$primary" : "$border"}>
133
+ <Box backgroundColor={isSelected ? "$primary" : undefined} paddingX={1}>
134
+ <Text bold color={isSelected ? "black" : "$text"}>
135
+ {column.title}
136
+ </Text>
137
+ <Text color={isSelected ? "black" : "$muted"}> ({column.cards.length})</Text>
138
+ </Box>
139
+
140
+ <Box
141
+ flexDirection="column"
142
+ paddingX={1}
143
+ overflow="scroll"
144
+ scrollTo={isSelected ? selectedCardIndex : undefined}
145
+ flexGrow={1}
146
+ gap={1}
147
+ >
148
+ {column.cards.map((card, cardIndex) => (
149
+ <CardComponent key={card.id} card={card} isSelected={isSelected && cardIndex === selectedCardIndex} />
150
+ ))}
151
+
152
+ {column.cards.length === 0 && (
153
+ <Text dim italic>
154
+ No cards
155
+ </Text>
156
+ )}
157
+ </Box>
158
+ </Box>
159
+ )
160
+ }
161
+
162
+ function HelpBar() {
163
+ return (
164
+ <Muted>
165
+ {" "}
166
+ <Kbd>h/l</Kbd> column <Kbd>j/k</Kbd> card <Kbd>{"</"}</Kbd> move <Kbd>Esc/q</Kbd> quit
167
+ </Muted>
168
+ )
169
+ }
170
+
171
+ export function KanbanBoard() {
172
+ const { exit } = useApp()
173
+ const [columns, setColumns] = useState<Column[]>(initialColumns)
174
+ const [selectedColumn, setSelectedColumn] = useState(0)
175
+ const [selectedCard, setSelectedCard] = useState(0)
176
+
177
+ const currentColumn = columns[selectedColumn]
178
+ const currentColumnCards = currentColumn?.cards ?? []
179
+ const boundedSelectedCard = Math.min(selectedCard, Math.max(0, currentColumnCards.length - 1))
180
+
181
+ useInput((input: string, key: Key) => {
182
+ if (input === "q" || key.escape) {
183
+ exit()
184
+ }
185
+
186
+ // Column navigation
187
+ if (key.leftArrow || input === "h") {
188
+ setSelectedColumn((prev) => Math.max(0, prev - 1))
189
+ setSelectedCard(0)
190
+ }
191
+ if (key.rightArrow || input === "l") {
192
+ setSelectedColumn((prev) => Math.min(columns.length - 1, prev + 1))
193
+ setSelectedCard(0)
194
+ }
195
+
196
+ // Card navigation
197
+ if (key.upArrow || input === "k") {
198
+ setSelectedCard((prev) => Math.max(0, prev - 1))
199
+ }
200
+ if (key.downArrow || input === "j") {
201
+ setSelectedCard((prev) => Math.min(currentColumnCards.length - 1, prev + 1))
202
+ }
203
+
204
+ // Move card between columns
205
+ if (input === "<" || input === ",") {
206
+ moveCard(-1)
207
+ }
208
+ if (input === ">" || input === ".") {
209
+ moveCard(1)
210
+ }
211
+ })
212
+
213
+ function moveCard(direction: number): void {
214
+ const targetColumnIndex = selectedColumn + direction
215
+ if (targetColumnIndex < 0 || targetColumnIndex >= columns.length) return
216
+ if (currentColumnCards.length === 0) return
217
+
218
+ const cardToMove = currentColumnCards[boundedSelectedCard]
219
+ if (!cardToMove) return
220
+
221
+ setColumns((prev) => {
222
+ const next = prev.map((col) => ({ ...col, cards: [...col.cards] }))
223
+ next[selectedColumn]!.cards.splice(boundedSelectedCard, 1)
224
+ next[targetColumnIndex]!.cards.push(cardToMove)
225
+ return next
226
+ })
227
+
228
+ setSelectedColumn(targetColumnIndex)
229
+ setSelectedCard(columns[targetColumnIndex]!.cards.length)
230
+ }
231
+
232
+ return (
233
+ <Box flexDirection="column" padding={1} height="100%">
234
+ <Box flexGrow={1} flexDirection="row" gap={1} overflow="hidden">
235
+ {columns.map((column, colIndex) => (
236
+ <ColumnComponent
237
+ key={column.id}
238
+ column={column}
239
+ isSelected={colIndex === selectedColumn}
240
+ selectedCardIndex={colIndex === selectedColumn ? boundedSelectedCard : -1}
241
+ />
242
+ ))}
243
+ </Box>
244
+
245
+ <HelpBar />
246
+ </Box>
247
+ )
248
+ }
249
+
250
+ // ============================================================================
251
+ // Main
252
+ // ============================================================================
253
+
254
+ async function main() {
255
+ using term = createTerm()
256
+ const { waitUntilExit } = await render(
257
+ <ExampleBanner meta={meta} controls="h/l column j/k card </> move Esc/q quit">
258
+ <KanbanBoard />
259
+ </ExampleBanner>,
260
+ term,
261
+ )
262
+ await waitUntilExit()
263
+ }
264
+
265
+ if (import.meta.main) {
266
+ main().catch(console.error)
267
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Layout Ref Example
3
+ *
4
+ * Demonstrates imperative access to layout information:
5
+ * - forwardRef on Box and Text components
6
+ * - BoxHandle for accessing layout info imperatively
7
+ * - onLayout callback for responding to size changes
8
+ */
9
+
10
+ import React, { useRef, useState, useEffect } from "react"
11
+ import {
12
+ render,
13
+ Box,
14
+ Text,
15
+ H1,
16
+ Kbd,
17
+ Muted,
18
+ useInput,
19
+ useApp,
20
+ createTerm,
21
+ type BoxHandle,
22
+ type Key,
23
+ } from "../../src/index.js"
24
+ import { ExampleBanner, type ExampleMeta } from "../_banner.js"
25
+
26
+ export const meta: ExampleMeta = {
27
+ name: "Layout Ref",
28
+ description: "useContentRect + useScreenRect for imperative layout measurement",
29
+ features: ["forwardRef", "BoxHandle", "onLayout", "getContentRect()"],
30
+ }
31
+
32
+ // ============================================================================
33
+ // Components
34
+ // ============================================================================
35
+
36
+ interface LayoutInfo {
37
+ x: number
38
+ y: number
39
+ width: number
40
+ height: number
41
+ }
42
+
43
+ function ResizablePane({
44
+ title,
45
+ color,
46
+ onLayoutChange,
47
+ }: {
48
+ title: string
49
+ color: string
50
+ onLayoutChange: (info: LayoutInfo) => void
51
+ }) {
52
+ const boxRef = useRef<BoxHandle>(null)
53
+
54
+ // onLayout callback fires when this Box's dimensions change
55
+ return (
56
+ <Box
57
+ ref={boxRef}
58
+ flexGrow={1}
59
+ borderStyle="round"
60
+ borderColor={color}
61
+ padding={1}
62
+ onLayout={(layout) => onLayoutChange(layout)}
63
+ >
64
+ <H1 color={color}>{title}</H1>
65
+ </Box>
66
+ )
67
+ }
68
+
69
+ function ImperativeAccessDemo() {
70
+ const boxRef = useRef<BoxHandle>(null)
71
+ const [info, setInfo] = useState<string>("Click 'i' to inspect")
72
+
73
+ const inspect = () => {
74
+ if (!boxRef.current) {
75
+ setInfo("No ref attached")
76
+ return
77
+ }
78
+
79
+ const content = boxRef.current.getContentRect()
80
+ const screen = boxRef.current.getScreenRect()
81
+ const node = boxRef.current.getNode()
82
+
83
+ setInfo(
84
+ `Content: ${content?.width}x${content?.height} at (${content?.x},${content?.y})\n` +
85
+ `Screen: ${screen?.width}x${screen?.height} at (${screen?.x},${screen?.y})\n` +
86
+ `Node: ${node ? "available" : "null"}`,
87
+ )
88
+ }
89
+
90
+ return (
91
+ <Box ref={boxRef} flexDirection="column" borderStyle="double" borderColor="magenta" padding={1}>
92
+ <H1 color="magenta">Imperative Access (BoxHandle)</H1>
93
+ <Muted>Press 'i' to inspect this box</Muted>
94
+ <Box marginTop={1}>
95
+ <Text>{info}</Text>
96
+ </Box>
97
+ {/* Expose inspect function via closure */}
98
+ <InspectTrigger onInspect={inspect} />
99
+ </Box>
100
+ )
101
+ }
102
+
103
+ // Hidden component to trigger inspect on keypress
104
+ function InspectTrigger({ onInspect }: { onInspect: () => void }) {
105
+ useInput((input: string) => {
106
+ if (input === "i") {
107
+ onInspect()
108
+ }
109
+ })
110
+ return null
111
+ }
112
+
113
+ export function LayoutRefApp() {
114
+ const { exit } = useApp()
115
+ const [layouts, setLayouts] = useState<Record<string, LayoutInfo>>({})
116
+
117
+ useInput((input: string, key: Key) => {
118
+ if (input === "q" || key.escape) {
119
+ exit()
120
+ }
121
+ })
122
+
123
+ const handleLayoutChange = (pane: string) => (info: LayoutInfo) => {
124
+ setLayouts((prev) => ({ ...prev, [pane]: info }))
125
+ }
126
+
127
+ return (
128
+ <Box flexDirection="column" padding={1}>
129
+ {/* Row of resizable panes with onLayout callbacks */}
130
+ <Box flexDirection="row" gap={1} height={8}>
131
+ <ResizablePane title="Pane A" color="green" onLayoutChange={handleLayoutChange("a")} />
132
+ <ResizablePane title="Pane B" color="blue" onLayoutChange={handleLayoutChange("b")} />
133
+ <ResizablePane title="Pane C" color="cyan" onLayoutChange={handleLayoutChange("c")} />
134
+ </Box>
135
+
136
+ {/* Show layout info from onLayout callbacks */}
137
+ <Box marginTop={1} borderStyle="single" borderColor="gray" padding={1}>
138
+ <Box flexDirection="column">
139
+ <Text bold dim>
140
+ onLayout Results:
141
+ </Text>
142
+ {Object.entries(layouts).map(([pane, info]) => (
143
+ <Text key={pane} dim>
144
+ Pane {pane.toUpperCase()}: {info.width}x{info.height} at ({info.x},{info.y})
145
+ </Text>
146
+ ))}
147
+ {Object.keys(layouts).length === 0 && (
148
+ <Text dim italic>
149
+ Waiting for layout...
150
+ </Text>
151
+ )}
152
+ </Box>
153
+ </Box>
154
+
155
+ {/* Imperative access demo */}
156
+ <Box flexGrow={1} marginTop={1}>
157
+ <ImperativeAccessDemo />
158
+ </Box>
159
+
160
+ <Muted>
161
+ {" "}
162
+ <Kbd>i</Kbd> inspect <Kbd>Esc/q</Kbd> quit
163
+ </Muted>
164
+ </Box>
165
+ )
166
+ }
167
+
168
+ // ============================================================================
169
+ // Main
170
+ // ============================================================================
171
+
172
+ async function main() {
173
+ using term = createTerm()
174
+ const { waitUntilExit } = await render(
175
+ <ExampleBanner meta={meta} controls="i inspect Esc/q quit">
176
+ <LayoutRefApp />
177
+ </ExampleBanner>,
178
+ term,
179
+ )
180
+ await waitUntilExit()
181
+ }
182
+
183
+ if (import.meta.main) {
184
+ main().catch(console.error)
185
+ }