@silvery/examples 0.4.4 → 0.5.1
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/README.md +7 -0
- package/examples/_banner.tsx +1 -1
- package/examples/apps/aichat/index.tsx +47 -35
- package/examples/apps/aichat/state.ts +3 -4
- package/examples/apps/app-todo.tsx +2 -2
- package/examples/apps/async-data.tsx +1 -13
- package/examples/apps/components.tsx +4 -4
- package/examples/apps/data-explorer.tsx +10 -10
- package/examples/apps/dev-tools.tsx +10 -10
- package/examples/apps/explorer.tsx +12 -12
- package/examples/apps/gallery.tsx +4 -4
- package/examples/apps/inline-bench.tsx +2 -2
- package/examples/apps/layout-ref.tsx +5 -17
- package/examples/apps/outline.tsx +4 -15
- package/examples/apps/panes/index.tsx +7 -9
- package/examples/apps/paste-demo.tsx +1 -14
- package/examples/apps/task-list.tsx +2 -2
- package/examples/apps/textarea.tsx +2 -14
- package/examples/apps/transform.tsx +1 -14
- package/examples/apps/virtual-10k.tsx +11 -11
- package/examples/components/virtual-list.tsx +9 -9
- package/package.json +11 -10
package/README.md
ADDED
package/examples/_banner.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { Box, Text, Strong, Muted, ThemeProvider, getThemeByName, type Theme } f
|
|
|
4
4
|
export interface ExampleMeta {
|
|
5
5
|
name: string
|
|
6
6
|
description: string
|
|
7
|
-
/** API features showcased, e.g. ["
|
|
7
|
+
/** API features showcased, e.g. ["ListView", "useBoxRect()"] */
|
|
8
8
|
features?: string[]
|
|
9
9
|
/** Curated demo — shown in CLI viewer (`bun examples`) and web showcase */
|
|
10
10
|
demo?: boolean
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI Chat — Coding Agent Demo
|
|
3
3
|
*
|
|
4
|
-
* Showcases
|
|
5
|
-
* TEA state machine drives all animation;
|
|
6
|
-
* exchanges
|
|
4
|
+
* Showcases ListView with streaming, tool calls, context tracking.
|
|
5
|
+
* TEA state machine drives all animation; ListView caches completed
|
|
6
|
+
* exchanges while live content stays in the React tree.
|
|
7
7
|
*
|
|
8
8
|
* Flags: --auto (auto-advance) --fast (skip animation) --stress (200 exchanges)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import React, { useEffect, useRef, useMemo } from "react"
|
|
12
|
-
import { Box, Text, Spinner,
|
|
11
|
+
import React, { useCallback, useEffect, useRef, useMemo } from "react"
|
|
12
|
+
import { Box, Text, Spinner, ListView, useTea } from "silvery"
|
|
13
|
+
import type { ListItemMeta } from "silvery"
|
|
13
14
|
import { run, useInput, useExit, type Key } from "silvery/runtime"
|
|
14
15
|
import type { ExampleMeta } from "../../_banner.js"
|
|
15
16
|
import type { ScriptEntry } from "./types.js"
|
|
@@ -32,13 +33,14 @@ export type { Exchange, ToolCall } from "./types.js"
|
|
|
32
33
|
|
|
33
34
|
export const meta: ExampleMeta = {
|
|
34
35
|
name: "AI Coding Agent",
|
|
35
|
-
description: "Coding agent showcase —
|
|
36
|
+
description: "Coding agent showcase — ListView, streaming, context tracking",
|
|
36
37
|
demo: true,
|
|
37
|
-
features: ["
|
|
38
|
+
features: ["ListView", "cache", "inline mode", "streaming", "OSC 8 links"],
|
|
39
|
+
// TODO: Add OSC 133 marker support to ListView (km-silvery.listview-markers)
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
// ============================================================================
|
|
41
|
-
// AIChat — TEA state machine +
|
|
43
|
+
// AIChat — TEA state machine + ListView
|
|
42
44
|
// ============================================================================
|
|
43
45
|
|
|
44
46
|
export function AIChat({
|
|
@@ -60,13 +62,45 @@ export function AIChat({
|
|
|
60
62
|
useAutoExit(autoStart, state.done, exit)
|
|
61
63
|
useKeyBindings(state, send, footerControlRef)
|
|
62
64
|
|
|
65
|
+
const renderExchange = useCallback(
|
|
66
|
+
(exchange: (typeof state.exchanges)[number], index: number, _meta: ListItemMeta) => {
|
|
67
|
+
const isLatest = index === state.exchanges.length - 1
|
|
68
|
+
return (
|
|
69
|
+
<Box flexDirection="column">
|
|
70
|
+
{index > 0 && <Text> </Text>}
|
|
71
|
+
{state.compacting && isLatest && <CompactingOverlay />}
|
|
72
|
+
{state.done && autoStart && isLatest && <SessionComplete />}
|
|
73
|
+
<ExchangeItem
|
|
74
|
+
exchange={exchange}
|
|
75
|
+
streamPhase={state.streamPhase}
|
|
76
|
+
revealFraction={state.revealFraction}
|
|
77
|
+
pulse={state.pulse}
|
|
78
|
+
isLatest={isLatest}
|
|
79
|
+
isFirstInGroup={exchange.role !== (index > 0 ? state.exchanges[index - 1]!.role : null)}
|
|
80
|
+
isLastInGroup={
|
|
81
|
+
exchange.role !== (index < state.exchanges.length - 1 ? state.exchanges[index + 1]!.role : null)
|
|
82
|
+
}
|
|
83
|
+
/>
|
|
84
|
+
</Box>
|
|
85
|
+
)
|
|
86
|
+
},
|
|
87
|
+
[state, autoStart],
|
|
88
|
+
)
|
|
89
|
+
|
|
63
90
|
return (
|
|
64
91
|
<Box flexDirection="column" paddingX={1}>
|
|
65
|
-
<
|
|
92
|
+
<ListView
|
|
66
93
|
items={state.exchanges}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
getKey={(ex) => ex.id}
|
|
95
|
+
height={process.stdout.rows ?? 24}
|
|
96
|
+
estimateHeight={6}
|
|
97
|
+
renderItem={renderExchange}
|
|
98
|
+
scrollTo={state.exchanges.length - 1}
|
|
99
|
+
cache={{
|
|
100
|
+
mode: "virtual",
|
|
101
|
+
isCacheable: (_ex, index) => index < state.exchanges.length - 1,
|
|
102
|
+
}}
|
|
103
|
+
listFooter={
|
|
70
104
|
<DemoFooter
|
|
71
105
|
controlRef={footerControlRef}
|
|
72
106
|
onSubmit={(text) => send({ type: "submit", text })}
|
|
@@ -80,29 +114,7 @@ export function AIChat({
|
|
|
80
114
|
autoTypingText={state.autoTyping ? state.autoTyping.full.slice(0, state.autoTyping.revealed) : null}
|
|
81
115
|
/>
|
|
82
116
|
}
|
|
83
|
-
|
|
84
|
-
{(exchange, index) => {
|
|
85
|
-
const isLatest = index === state.exchanges.length - 1
|
|
86
|
-
return (
|
|
87
|
-
<Box flexDirection="column">
|
|
88
|
-
{index > 0 && <Text> </Text>}
|
|
89
|
-
{state.compacting && isLatest && <CompactingOverlay />}
|
|
90
|
-
{state.done && autoStart && isLatest && <SessionComplete />}
|
|
91
|
-
<ExchangeItem
|
|
92
|
-
exchange={exchange}
|
|
93
|
-
streamPhase={state.streamPhase}
|
|
94
|
-
revealFraction={state.revealFraction}
|
|
95
|
-
pulse={state.pulse}
|
|
96
|
-
isLatest={isLatest}
|
|
97
|
-
isFirstInGroup={exchange.role !== (index > 0 ? state.exchanges[index - 1]!.role : null)}
|
|
98
|
-
isLastInGroup={
|
|
99
|
-
exchange.role !== (index < state.exchanges.length - 1 ? state.exchanges[index + 1]!.role : null)
|
|
100
|
-
}
|
|
101
|
-
/>
|
|
102
|
-
</Box>
|
|
103
|
-
)
|
|
104
|
-
}}
|
|
105
|
-
</ScrollbackList>
|
|
117
|
+
/>
|
|
106
118
|
</Box>
|
|
107
119
|
)
|
|
108
120
|
}
|
|
@@ -56,11 +56,10 @@ export type DemoResult = TeaResult<DemoState, DemoEffect>
|
|
|
56
56
|
// ============================================================================
|
|
57
57
|
|
|
58
58
|
const INTRO_TEXT = [
|
|
59
|
-
"Coding agent simulation showcasing
|
|
60
|
-
" •
|
|
61
|
-
" •
|
|
59
|
+
"Coding agent simulation showcasing ListView:",
|
|
60
|
+
" • ListView — unified virtualized list with cache",
|
|
61
|
+
" • Cache mode — completed exchanges cached for performance",
|
|
62
62
|
" • OSC 8 hyperlinks — clickable file paths and URLs",
|
|
63
|
-
" • OSC 133 markers — Cmd+↑/↓ to jump between exchanges",
|
|
64
63
|
" • $token theme colors — semantic color tokens",
|
|
65
64
|
].join("\n")
|
|
66
65
|
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
|
|
25
25
|
import React from "react"
|
|
26
26
|
import { Box, Text, Muted, Kbd } from "silvery"
|
|
27
|
-
import { createApp, useApp, type AppHandle } from "@silvery/
|
|
28
|
-
import { pipe, withReact, withTerminal } from "@silvery/
|
|
27
|
+
import { createApp, useApp, type AppHandle } from "@silvery/create/create-app"
|
|
28
|
+
import { pipe, withReact, withTerminal } from "@silvery/create/plugins"
|
|
29
29
|
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
30
30
|
|
|
31
31
|
export const meta: ExampleMeta = {
|
|
@@ -8,19 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React, { Suspense, useState, use } from "react"
|
|
11
|
-
import {
|
|
12
|
-
render,
|
|
13
|
-
Box,
|
|
14
|
-
Text,
|
|
15
|
-
H1,
|
|
16
|
-
Kbd,
|
|
17
|
-
Muted,
|
|
18
|
-
useInput,
|
|
19
|
-
useApp,
|
|
20
|
-
createTerm,
|
|
21
|
-
ErrorBoundary,
|
|
22
|
-
type Key,
|
|
23
|
-
} from "silvery"
|
|
11
|
+
import { render, Box, Text, H1, Kbd, Muted, useInput, useApp, createTerm, ErrorBoundary, type Key } from "silvery"
|
|
24
12
|
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
25
13
|
|
|
26
14
|
export const meta: ExampleMeta = {
|
|
@@ -87,8 +87,8 @@ function TypographyTab() {
|
|
|
87
87
|
|
|
88
88
|
<H3>Use Built-in Components</H3>
|
|
89
89
|
<P>
|
|
90
|
-
<Code>silvery/ui</Code> ships 30+ components. They handle keyboard navigation, theming,
|
|
91
|
-
|
|
90
|
+
<Code>silvery/ui</Code> ships 30+ components. They handle keyboard navigation, theming, mouse support, and
|
|
91
|
+
dozens of edge cases.
|
|
92
92
|
</P>
|
|
93
93
|
<UL>
|
|
94
94
|
<LI>
|
|
@@ -133,8 +133,8 @@ function TypographyTab() {
|
|
|
133
133
|
|
|
134
134
|
<H3>Think in Flexbox</H3>
|
|
135
135
|
<P>
|
|
136
|
-
Silvery uses CSS flexbox via Flexily. Components know their size via <Code>
|
|
137
|
-
|
|
136
|
+
Silvery uses CSS flexbox via Flexily. Components know their size via <Code>useBoxRect()</Code> — synchronous,
|
|
137
|
+
during render. No effects, no flash.
|
|
138
138
|
</P>
|
|
139
139
|
|
|
140
140
|
<Small>Last updated: silvery v0.0.1 — see silvery.dev for full documentation</Small>
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Data Explorer — Process Table Example
|
|
3
3
|
*
|
|
4
4
|
* A process explorer with a searchable, scrollable table demonstrating:
|
|
5
|
-
* - Table-like display with responsive column widths via
|
|
5
|
+
* - Table-like display with responsive column widths via useBoxRect()
|
|
6
6
|
* - TextInput for live search/filter with useDeferredValue
|
|
7
|
-
* -
|
|
7
|
+
* - ListView for smooth scrolling through 500+ rows
|
|
8
8
|
* - Keyboard navigation with j/k and vim-style jumps
|
|
9
9
|
* - Color-coded status indicators
|
|
10
10
|
*
|
|
@@ -24,10 +24,10 @@ import {
|
|
|
24
24
|
render,
|
|
25
25
|
Box,
|
|
26
26
|
Text,
|
|
27
|
-
|
|
27
|
+
ListView,
|
|
28
28
|
TextInput,
|
|
29
29
|
Divider,
|
|
30
|
-
|
|
30
|
+
useBoxRect,
|
|
31
31
|
useInput,
|
|
32
32
|
useApp,
|
|
33
33
|
createTerm,
|
|
@@ -40,8 +40,8 @@ import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
|
40
40
|
|
|
41
41
|
export const meta: ExampleMeta = {
|
|
42
42
|
name: "Data Explorer",
|
|
43
|
-
description: "Process explorer table with search,
|
|
44
|
-
features: ["
|
|
43
|
+
description: "Process explorer table with search, ListView, and responsive column widths",
|
|
44
|
+
features: ["useBoxRect()", "TextInput", "useInput()", "responsive layout", "useDeferredValue"],
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// ============================================================================
|
|
@@ -300,13 +300,13 @@ function SummaryBar({ processes, query }: { processes: ProcessInfo[]; query: str
|
|
|
300
300
|
|
|
301
301
|
/** Inner component that reads the flex container's height */
|
|
302
302
|
function ProcessListArea({ processes, cursor, width }: { processes: ProcessInfo[]; cursor: number; width: number }) {
|
|
303
|
-
const { height } =
|
|
303
|
+
const { height } = useBoxRect()
|
|
304
304
|
|
|
305
305
|
return (
|
|
306
|
-
<
|
|
306
|
+
<ListView
|
|
307
307
|
items={processes}
|
|
308
308
|
height={height}
|
|
309
|
-
|
|
309
|
+
estimateHeight={1}
|
|
310
310
|
scrollTo={cursor}
|
|
311
311
|
overscan={5}
|
|
312
312
|
renderItem={(proc, index) => (
|
|
@@ -322,7 +322,7 @@ function ProcessListArea({ processes, cursor, width }: { processes: ProcessInfo[
|
|
|
322
322
|
|
|
323
323
|
export function DataExplorer() {
|
|
324
324
|
const { exit } = useApp()
|
|
325
|
-
const { width } =
|
|
325
|
+
const { width } = useBoxRect()
|
|
326
326
|
const [cursor, setCursor] = useState(0)
|
|
327
327
|
const [searchMode, setSearchMode] = useState(false)
|
|
328
328
|
const [query, setQuery] = useState("")
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Dev Tools — Log Viewer Example
|
|
3
3
|
*
|
|
4
4
|
* A live log viewer demonstrating:
|
|
5
|
-
* -
|
|
5
|
+
* - ListView for efficient rendering of thousands of log entries
|
|
6
6
|
* - Keyboard shortcuts to add log entries at different severity levels
|
|
7
7
|
* - Color-coded severity levels (DEBUG, INFO, WARN, ERROR)
|
|
8
8
|
* - j/k navigation through log history
|
|
@@ -26,9 +26,9 @@ import {
|
|
|
26
26
|
render,
|
|
27
27
|
Box,
|
|
28
28
|
Text,
|
|
29
|
-
|
|
29
|
+
ListView,
|
|
30
30
|
Divider,
|
|
31
|
-
|
|
31
|
+
useBoxRect,
|
|
32
32
|
useInput,
|
|
33
33
|
useApp,
|
|
34
34
|
createTerm,
|
|
@@ -42,8 +42,8 @@ import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
|
42
42
|
|
|
43
43
|
export const meta: ExampleMeta = {
|
|
44
44
|
name: "Dev Tools",
|
|
45
|
-
description: "Log viewer with severity levels,
|
|
46
|
-
features: ["
|
|
45
|
+
description: "Log viewer with severity levels, ListView, and keyboard-driven log injection",
|
|
46
|
+
features: ["ListView", "useInput()", "useBoxRect()", "keyboard navigation"],
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// ============================================================================
|
|
@@ -215,15 +215,15 @@ function LevelCounts({ entries }: { entries: LogEntry[] }) {
|
|
|
215
215
|
)
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
/** Inner component that reads the flex container's height via
|
|
218
|
+
/** Inner component that reads the flex container's height via useBoxRect */
|
|
219
219
|
function LogListArea({ entries, cursor }: { entries: LogEntry[]; cursor: number }) {
|
|
220
|
-
const { height } =
|
|
220
|
+
const { height } = useBoxRect()
|
|
221
221
|
|
|
222
222
|
return (
|
|
223
|
-
<
|
|
223
|
+
<ListView
|
|
224
224
|
items={entries}
|
|
225
225
|
height={height}
|
|
226
|
-
|
|
226
|
+
estimateHeight={1}
|
|
227
227
|
scrollTo={cursor}
|
|
228
228
|
overscan={5}
|
|
229
229
|
renderItem={(entry, index) => <LogRow key={entry.id} entry={entry} isSelected={index === cursor} />}
|
|
@@ -240,7 +240,7 @@ const rng = seededRandom(12345)
|
|
|
240
240
|
|
|
241
241
|
export function DevTools() {
|
|
242
242
|
const { exit } = useApp()
|
|
243
|
-
const { width } =
|
|
243
|
+
const { width } = useBoxRect()
|
|
244
244
|
const [entries, setEntries] = useState<LogEntry[]>(() => generateInitialLogs(INITIAL_COUNT))
|
|
245
245
|
const [cursor, setCursor] = useState(INITIAL_COUNT - 1)
|
|
246
246
|
const [autoScroll, setAutoScroll] = useState(true)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Streaming log viewer with ~2000 lines, severity-level coloring, and level toggles
|
|
6
6
|
* - Sortable process table with ~50 processes, live CPU/MEM jitter, and responsive columns
|
|
7
7
|
* - Shared TextInput search bar with useDeferredValue for non-blocking filtering
|
|
8
|
-
* -
|
|
8
|
+
* - ListView with interactive scrolling for both tabs
|
|
9
9
|
*
|
|
10
10
|
* Usage: bun vendor/silvery/examples/apps/explorer.tsx
|
|
11
11
|
*
|
|
@@ -26,13 +26,13 @@ import {
|
|
|
26
26
|
render,
|
|
27
27
|
Box,
|
|
28
28
|
Text,
|
|
29
|
-
|
|
29
|
+
ListView,
|
|
30
30
|
TextInput,
|
|
31
31
|
Tabs,
|
|
32
32
|
TabList,
|
|
33
33
|
Tab,
|
|
34
34
|
Divider,
|
|
35
|
-
|
|
35
|
+
useBoxRect,
|
|
36
36
|
useInput,
|
|
37
37
|
useApp,
|
|
38
38
|
createTerm,
|
|
@@ -44,9 +44,9 @@ import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
|
44
44
|
|
|
45
45
|
export const meta: ExampleMeta = {
|
|
46
46
|
name: "Explorer",
|
|
47
|
-
description: "Log viewer and process explorer with
|
|
47
|
+
description: "Log viewer and process explorer with ListView search",
|
|
48
48
|
demo: true,
|
|
49
|
-
features: ["
|
|
49
|
+
features: ["ListView", "TextInput", "useBoxRect()", "useDeferredValue", "2000+ rows"],
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// ============================================================================
|
|
@@ -297,13 +297,13 @@ function LogRow({ entry, isSelected }: { entry: LogEntry; isSelected: boolean })
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
function LogListArea({ entries, cursor }: { entries: LogEntry[]; cursor: number }) {
|
|
300
|
-
const { height } =
|
|
300
|
+
const { height } = useBoxRect()
|
|
301
301
|
|
|
302
302
|
return (
|
|
303
|
-
<
|
|
303
|
+
<ListView
|
|
304
304
|
items={entries}
|
|
305
305
|
height={height}
|
|
306
|
-
|
|
306
|
+
estimateHeight={1}
|
|
307
307
|
scrollTo={cursor}
|
|
308
308
|
overscan={5}
|
|
309
309
|
renderItem={(entry, index) => <LogRow key={entry.id} entry={entry} isSelected={index === cursor} />}
|
|
@@ -390,13 +390,13 @@ function ProcessRow({ proc, isSelected, width }: { proc: ProcessInfo; isSelected
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
function ProcessListArea({ processes, cursor, width }: { processes: ProcessInfo[]; cursor: number; width: number }) {
|
|
393
|
-
const { height } =
|
|
393
|
+
const { height } = useBoxRect()
|
|
394
394
|
|
|
395
395
|
return (
|
|
396
|
-
<
|
|
396
|
+
<ListView
|
|
397
397
|
items={processes}
|
|
398
398
|
height={height}
|
|
399
|
-
|
|
399
|
+
estimateHeight={1}
|
|
400
400
|
scrollTo={cursor}
|
|
401
401
|
overscan={5}
|
|
402
402
|
renderItem={(proc, index) => (
|
|
@@ -412,7 +412,7 @@ function ProcessListArea({ processes, cursor, width }: { processes: ProcessInfo[
|
|
|
412
412
|
|
|
413
413
|
export function Explorer() {
|
|
414
414
|
const { exit } = useApp()
|
|
415
|
-
const { width } =
|
|
415
|
+
const { width } = useBoxRect()
|
|
416
416
|
|
|
417
417
|
// Tab state
|
|
418
418
|
const [activeTab, setActiveTab] = useState("logs")
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
H2,
|
|
27
27
|
useInput,
|
|
28
28
|
useApp,
|
|
29
|
-
|
|
29
|
+
useBoxRect,
|
|
30
30
|
createTerm,
|
|
31
31
|
type Key,
|
|
32
32
|
} from "silvery"
|
|
@@ -222,7 +222,7 @@ function generateCheckerPattern(w: number, h: number): Buffer {
|
|
|
222
222
|
// ============================================================================
|
|
223
223
|
|
|
224
224
|
function ImagesTab() {
|
|
225
|
-
const rect =
|
|
225
|
+
const rect = useBoxRect()
|
|
226
226
|
const w = Math.max(20, rect.width - 4)
|
|
227
227
|
const imgH = Math.max(5, rect.height - 6)
|
|
228
228
|
|
|
@@ -311,7 +311,7 @@ const PAINT_PRESETS: { name: string; color: RGB }[] = [
|
|
|
311
311
|
]
|
|
312
312
|
|
|
313
313
|
function PaintTab() {
|
|
314
|
-
const rect =
|
|
314
|
+
const rect = useBoxRect()
|
|
315
315
|
const canvasW = Math.max(10, rect.width - 2)
|
|
316
316
|
const canvasTermH = Math.max(4, rect.height - 7)
|
|
317
317
|
const canvasPixH = canvasTermH * 2
|
|
@@ -457,7 +457,7 @@ function PaintTab() {
|
|
|
457
457
|
// ============================================================================
|
|
458
458
|
|
|
459
459
|
function TruecolorTab() {
|
|
460
|
-
const rect =
|
|
460
|
+
const rect = useBoxRect()
|
|
461
461
|
const w = Math.max(20, rect.width - 4)
|
|
462
462
|
const availH = Math.max(10, rect.height - 3)
|
|
463
463
|
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* bun examples/apps/inline-bench.tsx
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { TerminalBuffer } from "silvery"
|
|
13
|
-
import { createOutputPhase, outputPhase } from "silvery/pipeline/output-phase"
|
|
12
|
+
import { TerminalBuffer } from "@silvery/ag-term/buffer"
|
|
13
|
+
import { createOutputPhase, outputPhase } from "@silvery/ag-term/pipeline/output-phase"
|
|
14
14
|
|
|
15
15
|
const RUNS = 500
|
|
16
16
|
|
|
@@ -8,25 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
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 "silvery"
|
|
11
|
+
import { render, Box, Text, H1, Kbd, Muted, useInput, useApp, createTerm, type BoxHandle, type Key } from "silvery"
|
|
24
12
|
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
25
13
|
|
|
26
14
|
export const meta: ExampleMeta = {
|
|
27
15
|
name: "Layout Ref",
|
|
28
|
-
description: "
|
|
29
|
-
features: ["forwardRef", "BoxHandle", "onLayout", "
|
|
16
|
+
description: "useBoxRect + useScrollRect for imperative layout measurement",
|
|
17
|
+
features: ["forwardRef", "BoxHandle", "onLayout", "getBoxRect()"],
|
|
30
18
|
}
|
|
31
19
|
|
|
32
20
|
// ============================================================================
|
|
@@ -76,8 +64,8 @@ function ImperativeAccessDemo() {
|
|
|
76
64
|
return
|
|
77
65
|
}
|
|
78
66
|
|
|
79
|
-
const content = boxRef.current.
|
|
80
|
-
const screen = boxRef.current.
|
|
67
|
+
const content = boxRef.current.getBoxRect()
|
|
68
|
+
const screen = boxRef.current.getScrollRect()
|
|
81
69
|
const node = boxRef.current.getNode()
|
|
82
70
|
|
|
83
71
|
setInfo(
|
|
@@ -9,30 +9,19 @@
|
|
|
9
9
|
* - Left panel: Box with borderStyle — content area is smaller
|
|
10
10
|
* - Right panel: Box with outlineStyle — content starts at edge
|
|
11
11
|
* - Toggle between styles with Tab
|
|
12
|
-
* - Live content dimensions via
|
|
12
|
+
* - Live content dimensions via useBoxRect()
|
|
13
13
|
*
|
|
14
14
|
* Run: bun vendor/silvery/examples/apps/outline.tsx
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import React, { useState } from "react"
|
|
18
|
-
import {
|
|
19
|
-
render,
|
|
20
|
-
Box,
|
|
21
|
-
Text,
|
|
22
|
-
Kbd,
|
|
23
|
-
Muted,
|
|
24
|
-
useInput,
|
|
25
|
-
useApp,
|
|
26
|
-
useContentRect,
|
|
27
|
-
createTerm,
|
|
28
|
-
type Key,
|
|
29
|
-
} from "silvery"
|
|
18
|
+
import { render, Box, Text, Kbd, Muted, useInput, useApp, useBoxRect, createTerm, type Key } from "silvery"
|
|
30
19
|
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
31
20
|
|
|
32
21
|
export const meta: ExampleMeta = {
|
|
33
22
|
name: "Outline vs Border",
|
|
34
23
|
description: "Side-by-side comparison showing outline (no layout impact) vs border",
|
|
35
|
-
features: ["outlineStyle", "borderStyle", "
|
|
24
|
+
features: ["outlineStyle", "borderStyle", "useBoxRect()", "layout dimensions"],
|
|
36
25
|
}
|
|
37
26
|
|
|
38
27
|
// ============================================================================
|
|
@@ -48,7 +37,7 @@ const STYLES: StyleVariant[] = ["single", "double", "round", "bold"]
|
|
|
48
37
|
// ============================================================================
|
|
49
38
|
|
|
50
39
|
function ContentWithSize({ label }: { label: string }) {
|
|
51
|
-
const { width, height } =
|
|
40
|
+
const { width, height } = useBoxRect()
|
|
52
41
|
|
|
53
42
|
return (
|
|
54
43
|
<Box flexDirection="column">
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import React, { useState, useEffect, useMemo } from "react"
|
|
11
11
|
import { Box, Text, ListView } from "silvery"
|
|
12
|
-
import {
|
|
12
|
+
import { SearchProvider, SearchBar, useSearch } from "silvery"
|
|
13
13
|
import { run, useInput, type Key } from "silvery/runtime"
|
|
14
14
|
import type { ExampleMeta } from "../../_banner.js"
|
|
15
15
|
import { SCRIPT } from "../aichat/script.js"
|
|
@@ -85,11 +85,11 @@ function ChatPane({
|
|
|
85
85
|
scrollTo={exchanges.length - 1}
|
|
86
86
|
active={active}
|
|
87
87
|
surfaceId={surfaceId}
|
|
88
|
-
|
|
88
|
+
cache={{
|
|
89
89
|
mode: "virtual",
|
|
90
|
-
|
|
90
|
+
isCacheable: (_ex: Exchange, idx: number) => idx < exchanges.length - 1,
|
|
91
91
|
}}
|
|
92
|
-
|
|
92
|
+
search={{ getText: (ex: Exchange) => ex.content }}
|
|
93
93
|
renderItem={(exchange: Exchange, _index: number, _meta: ListItemMeta) => (
|
|
94
94
|
<ExchangeItem
|
|
95
95
|
exchange={exchange}
|
|
@@ -190,11 +190,9 @@ export async function main() {
|
|
|
190
190
|
const rows = process.stdout.rows ?? 40
|
|
191
191
|
|
|
192
192
|
using handle = await run(
|
|
193
|
-
<
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
</SearchProvider>
|
|
197
|
-
</SurfaceRegistryProvider>,
|
|
193
|
+
<SearchProvider>
|
|
194
|
+
<PanesApp fastMode={fastMode} rows={rows} />
|
|
195
|
+
</SearchProvider>,
|
|
198
196
|
{ mode: "fullscreen", kitty: false, textSizing: false },
|
|
199
197
|
)
|
|
200
198
|
await handle.waitUntilExit()
|
|
@@ -15,20 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import React, { useState } from "react"
|
|
18
|
-
import {
|
|
19
|
-
render,
|
|
20
|
-
Box,
|
|
21
|
-
Text,
|
|
22
|
-
H1,
|
|
23
|
-
Small,
|
|
24
|
-
Kbd,
|
|
25
|
-
Muted,
|
|
26
|
-
Lead,
|
|
27
|
-
useInput,
|
|
28
|
-
useApp,
|
|
29
|
-
createTerm,
|
|
30
|
-
type Key,
|
|
31
|
-
} from "silvery"
|
|
18
|
+
import { render, Box, Text, H1, Small, Kbd, Muted, Lead, useInput, useApp, createTerm, type Key } from "silvery"
|
|
32
19
|
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
33
20
|
|
|
34
21
|
export const meta: ExampleMeta = {
|
|
@@ -15,7 +15,7 @@ import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
|
15
15
|
export const meta: ExampleMeta = {
|
|
16
16
|
name: "Task List",
|
|
17
17
|
description: "Scrollable list with priority badges, toggles, and expandable subtasks",
|
|
18
|
-
features: ["
|
|
18
|
+
features: ["ListView", "variable estimateHeight", "Box overflow"],
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// ============================================================================
|
|
@@ -161,7 +161,7 @@ export function TaskList() {
|
|
|
161
161
|
const [cursor, setCursor] = useState(0)
|
|
162
162
|
const [expandedTasks, setExpandedTasks] = useState<Set<number>>(new Set())
|
|
163
163
|
|
|
164
|
-
// Fixed visible count (in a real app, this would use
|
|
164
|
+
// Fixed visible count (in a real app, this would use useBoxRect)
|
|
165
165
|
const visibleCount = 15
|
|
166
166
|
|
|
167
167
|
// Calculate scroll offset to keep cursor visible
|
|
@@ -10,25 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import React, { useState } from "react"
|
|
13
|
-
import {
|
|
14
|
-
render,
|
|
15
|
-
Box,
|
|
16
|
-
Text,
|
|
17
|
-
H1,
|
|
18
|
-
Strong,
|
|
19
|
-
Muted,
|
|
20
|
-
TextArea,
|
|
21
|
-
useInput,
|
|
22
|
-
useApp,
|
|
23
|
-
createTerm,
|
|
24
|
-
type Key,
|
|
25
|
-
} from "silvery"
|
|
13
|
+
import { render, Box, Text, H1, Strong, Muted, TextArea, useInput, useApp, createTerm, type Key } from "silvery"
|
|
26
14
|
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
27
15
|
|
|
28
16
|
export const meta: ExampleMeta = {
|
|
29
17
|
name: "TextArea",
|
|
30
18
|
description: "Multi-line text input with word wrap, scrolling, and kill operations",
|
|
31
|
-
features: ["TextArea", "
|
|
19
|
+
features: ["TextArea", "useBoxRect()", "Ctrl+Enter submit"],
|
|
32
20
|
}
|
|
33
21
|
|
|
34
22
|
export function NoteEditor() {
|
|
@@ -14,20 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import React, { useState } from "react"
|
|
17
|
-
import {
|
|
18
|
-
render,
|
|
19
|
-
Box,
|
|
20
|
-
Text,
|
|
21
|
-
H1,
|
|
22
|
-
Small,
|
|
23
|
-
Kbd,
|
|
24
|
-
Muted,
|
|
25
|
-
Transform,
|
|
26
|
-
useInput,
|
|
27
|
-
useApp,
|
|
28
|
-
createTerm,
|
|
29
|
-
type Key,
|
|
30
|
-
} from "silvery"
|
|
17
|
+
import { render, Box, Text, H1, Small, Kbd, Muted, Transform, useInput, useApp, createTerm, type Key } from "silvery"
|
|
31
18
|
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
32
19
|
|
|
33
20
|
export const meta: ExampleMeta = {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Virtual Scroll Benchmark — 10,000 Items
|
|
3
3
|
*
|
|
4
|
-
* Demonstrates that
|
|
4
|
+
* Demonstrates that ListView handles massive datasets with instant scrolling.
|
|
5
5
|
* Only visible items + overscan are rendered, regardless of total count.
|
|
6
6
|
*
|
|
7
7
|
* Demonstrates:
|
|
8
|
-
* -
|
|
8
|
+
* - ListView with 10,000 items and variable heights
|
|
9
9
|
* - Smooth j/k navigation with position indicator
|
|
10
|
-
* -
|
|
10
|
+
* - useBoxRect() for adaptive column count
|
|
11
11
|
* - Page up/down with large jumps
|
|
12
12
|
* - Visual item variety (priorities, tags, progress bars)
|
|
13
13
|
*
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import React, { useState, useCallback, useMemo } from "react"
|
|
25
|
-
import { Box, Text, Strong, Kbd, Muted, Divider,
|
|
25
|
+
import { Box, Text, Strong, Kbd, Muted, Divider, ListView, useBoxRect } from "silvery"
|
|
26
26
|
import { run, useInput, type Key } from "silvery/runtime"
|
|
27
27
|
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
28
28
|
|
|
29
29
|
export const meta: ExampleMeta = {
|
|
30
30
|
name: "Virtual 10K",
|
|
31
|
-
description: "
|
|
32
|
-
features: ["
|
|
31
|
+
description: "ListView scrolling through 10,000 items with instant navigation",
|
|
32
|
+
features: ["ListView", "10K items", "useBoxRect()", "variable estimateHeight"],
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// ============================================================================
|
|
@@ -287,7 +287,7 @@ function StatsBar({ items }: { items: Item[] }) {
|
|
|
287
287
|
// ============================================================================
|
|
288
288
|
|
|
289
289
|
function VirtualBenchmark() {
|
|
290
|
-
const { width, height } =
|
|
290
|
+
const { width, height } = useBoxRect()
|
|
291
291
|
const [cursor, setCursor] = useState(0)
|
|
292
292
|
const [showDetail, setShowDetail] = useState(false)
|
|
293
293
|
|
|
@@ -296,8 +296,8 @@ function VirtualBenchmark() {
|
|
|
296
296
|
const listHeight = Math.max(5, height - 5)
|
|
297
297
|
const halfPage = Math.max(1, Math.floor(listHeight / 2))
|
|
298
298
|
|
|
299
|
-
const
|
|
300
|
-
(
|
|
299
|
+
const estimateHeight = useCallback(
|
|
300
|
+
(index: number) => {
|
|
301
301
|
if (showDetail && index === cursor) return 2
|
|
302
302
|
return 1
|
|
303
303
|
},
|
|
@@ -356,10 +356,10 @@ function VirtualBenchmark() {
|
|
|
356
356
|
|
|
357
357
|
{/* Virtual list */}
|
|
358
358
|
<Box flexGrow={1}>
|
|
359
|
-
<
|
|
359
|
+
<ListView
|
|
360
360
|
items={ALL_ITEMS}
|
|
361
361
|
height={listHeight}
|
|
362
|
-
|
|
362
|
+
estimateHeight={estimateHeight}
|
|
363
363
|
scrollTo={cursor}
|
|
364
364
|
overscan={5}
|
|
365
365
|
renderItem={(item, index) => (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ListView — Efficient scrollable list
|
|
3
3
|
*
|
|
4
4
|
* Renders 200 items but only materializes visible rows.
|
|
5
5
|
* Built-in j/k navigation, page up/down, Home/End.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React from "react"
|
|
11
|
-
import { Box, Text,
|
|
11
|
+
import { Box, Text, ListView } from "silvery"
|
|
12
12
|
import { run, useInput } from "silvery/runtime"
|
|
13
13
|
|
|
14
14
|
const items = Array.from({ length: 200 }, (_, i) => ({
|
|
@@ -16,7 +16,7 @@ const items = Array.from({ length: 200 }, (_, i) => ({
|
|
|
16
16
|
name: `Item ${i + 1}`,
|
|
17
17
|
}))
|
|
18
18
|
|
|
19
|
-
function
|
|
19
|
+
function ListViewDemo() {
|
|
20
20
|
useInput((input, key) => {
|
|
21
21
|
if (input === "q" || key.escape) return "exit"
|
|
22
22
|
})
|
|
@@ -24,14 +24,14 @@ function VirtualListDemo() {
|
|
|
24
24
|
return (
|
|
25
25
|
<Box flexDirection="column" padding={1} gap={1}>
|
|
26
26
|
<Text bold>200 items (virtualized)</Text>
|
|
27
|
-
<
|
|
27
|
+
<ListView
|
|
28
28
|
items={items}
|
|
29
29
|
height={12}
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
estimateHeight={1}
|
|
31
|
+
nav
|
|
32
32
|
renderItem={(item, _index, meta) => (
|
|
33
|
-
<Text key={item.id} color={meta
|
|
34
|
-
{meta
|
|
33
|
+
<Text key={item.id} color={meta.isCursor ? "$primary" : undefined} bold={meta.isCursor}>
|
|
34
|
+
{meta.isCursor ? "> " : " "}
|
|
35
35
|
{item.name}
|
|
36
36
|
</Text>
|
|
37
37
|
)}
|
|
@@ -47,6 +47,6 @@ export const meta = {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
if (import.meta.main) {
|
|
50
|
-
const handle = await run(<
|
|
50
|
+
const handle = await run(<ListViewDemo />)
|
|
51
51
|
await handle.waitUntilExit()
|
|
52
52
|
}
|
package/package.json
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silvery/examples",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Example apps and component demos for silvery
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Example apps and component demos for silvery — bunx @silvery/examples <name>",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "
|
|
6
|
+
"author": "Bjørn Stabell <bjorn@stabell.org>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/beorn/silvery.git",
|
|
10
|
-
"directory": "packages/
|
|
10
|
+
"directory": "packages/examples"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"silvery-examples": "./bin/cli.ts"
|
|
11
14
|
},
|
|
12
15
|
"files": [
|
|
13
16
|
"bin",
|
|
14
17
|
"examples"
|
|
15
18
|
],
|
|
16
19
|
"type": "module",
|
|
17
|
-
"bin": {
|
|
18
|
-
"silvery-examples": "./bin/cli.ts"
|
|
19
|
-
},
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"silvery": "
|
|
25
|
-
"@silvery/
|
|
24
|
+
"@silvery/ag-term": "0.5.0",
|
|
25
|
+
"@silvery/create": "0.5.0",
|
|
26
|
+
"silvery": "0.11.1"
|
|
26
27
|
}
|
|
27
|
-
}
|
|
28
|
+
}
|