@silvery/examples 0.17.3 → 0.17.4
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-Cy7ViL8f.mjs +5074 -0
- package/dist/__vite-browser-external-2447137e-BML7CYau.mjs +4 -0
- package/dist/_banner-DLPxCqVy.mjs +44 -0
- package/dist/ansi-CCE2pVS0.mjs +16397 -0
- package/dist/apng-HhhBjRGt.mjs +68 -0
- package/dist/apng-mwUQbTTF.mjs +3 -0
- package/dist/apps/aichat/index.mjs +1299 -0
- package/dist/apps/app-todo.mjs +139 -0
- package/dist/apps/async-data.mjs +204 -0
- package/dist/apps/cli-wizard.mjs +339 -0
- package/dist/apps/clipboard.mjs +198 -0
- package/dist/apps/components.mjs +864 -0
- package/dist/apps/data-explorer.mjs +483 -0
- package/dist/apps/dev-tools.mjs +397 -0
- package/dist/apps/explorer.mjs +698 -0
- package/dist/apps/gallery.mjs +766 -0
- package/dist/apps/inline-bench.mjs +115 -0
- package/dist/apps/kanban.mjs +280 -0
- package/dist/apps/layout-ref.mjs +187 -0
- package/dist/apps/outline.mjs +203 -0
- package/dist/apps/paste-demo.mjs +189 -0
- package/dist/apps/scroll.mjs +86 -0
- package/dist/apps/search-filter.mjs +287 -0
- package/dist/apps/selection.mjs +355 -0
- package/dist/apps/spatial-focus-demo.mjs +388 -0
- package/dist/apps/task-list.mjs +258 -0
- package/dist/apps/terminal-caps-demo.mjs +315 -0
- package/dist/apps/terminal.mjs +872 -0
- package/dist/apps/text-selection-demo.mjs +254 -0
- package/dist/apps/textarea.mjs +178 -0
- package/dist/apps/theme.mjs +661 -0
- package/dist/apps/transform.mjs +215 -0
- package/dist/apps/virtual-10k.mjs +422 -0
- package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
- package/dist/backends-Bahh9mKN.mjs +1179 -0
- package/dist/backends-CCtCDQ94.mjs +3 -0
- package/dist/{cli.mjs → bin/cli.mjs} +15 -19
- package/dist/chunk-BSw8zbkd.mjs +37 -0
- package/dist/components/counter.mjs +48 -0
- package/dist/components/hello.mjs +31 -0
- package/dist/components/progress-bar.mjs +59 -0
- package/dist/components/select-list.mjs +85 -0
- package/dist/components/spinner.mjs +57 -0
- package/dist/components/text-input.mjs +62 -0
- package/dist/components/virtual-list.mjs +51 -0
- package/dist/flexily-zero-adapter-UB-ra8fR.mjs +3374 -0
- package/dist/gif-BZaqPPVX.mjs +3 -0
- package/dist/gif-BtnXuxLF.mjs +71 -0
- package/dist/gifenc-CLRW41dk.mjs +728 -0
- package/dist/jsx-runtime-dMs_8fNu.mjs +241 -0
- package/dist/key-mapping-5oYQdAQE.mjs +3 -0
- package/dist/key-mapping-D4LR1go6.mjs +130 -0
- package/dist/layout/dashboard.mjs +1204 -0
- package/dist/layout/live-resize.mjs +303 -0
- package/dist/layout/overflow.mjs +70 -0
- package/dist/layout/text-layout.mjs +335 -0
- package/dist/node-NuJ94BWl.mjs +1083 -0
- package/dist/plugins-D1KtkT4a.mjs +3057 -0
- package/dist/resvg-js-C_8Wps1F.mjs +201 -0
- package/dist/src-BTEVGpd9.mjs +23538 -0
- package/dist/src-CUUOuRH6.mjs +5322 -0
- package/dist/src-CzfRafCQ.mjs +814 -0
- package/dist/usingCtx-CsEf0xO3.mjs +57 -0
- package/dist/yoga-adapter-BVtQ5OJR.mjs +237 -0
- package/package.json +18 -13
- 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
package/apps/inline-bench.tsx
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
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
|
-
export 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
|
-
if (import.meta.main) {
|
|
137
|
-
await main()
|
|
138
|
-
}
|
package/apps/kanban.tsx
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
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, useInput, useApp, createTerm, type Key } from "silvery"
|
|
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"}>
|
|
106
|
-
{isSelected ? (
|
|
107
|
-
<Box backgroundColor="$primary" paddingX={1}>
|
|
108
|
-
<Text color="$primary-fg" bold wrap="truncate">
|
|
109
|
-
{card.title}
|
|
110
|
-
</Text>
|
|
111
|
-
</Box>
|
|
112
|
-
) : (
|
|
113
|
-
<Box paddingX={1}>
|
|
114
|
-
<Text wrap="truncate">{card.title}</Text>
|
|
115
|
-
</Box>
|
|
116
|
-
)}
|
|
117
|
-
<Box gap={1} paddingX={1}>
|
|
118
|
-
{card.tags.map((tag) => (
|
|
119
|
-
<Tag key={tag} name={tag} />
|
|
120
|
-
))}
|
|
121
|
-
</Box>
|
|
122
|
-
</Box>
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function ColumnComponent({
|
|
127
|
-
column,
|
|
128
|
-
isSelected,
|
|
129
|
-
selectedCardIndex,
|
|
130
|
-
}: {
|
|
131
|
-
column: Column
|
|
132
|
-
isSelected: boolean
|
|
133
|
-
selectedCardIndex: number
|
|
134
|
-
}) {
|
|
135
|
-
return (
|
|
136
|
-
<Box
|
|
137
|
-
flexDirection="column"
|
|
138
|
-
flexGrow={1}
|
|
139
|
-
flexBasis={0}
|
|
140
|
-
borderStyle="single"
|
|
141
|
-
borderColor={isSelected ? "$primary" : "$border"}
|
|
142
|
-
>
|
|
143
|
-
<Box backgroundColor={isSelected ? "$primary" : undefined} paddingX={1}>
|
|
144
|
-
<Text bold color={isSelected ? "$primary-fg" : "$text"}>
|
|
145
|
-
{column.title}
|
|
146
|
-
</Text>
|
|
147
|
-
<Text color={isSelected ? "$primary-fg" : "$muted"}> ({column.cards.length})</Text>
|
|
148
|
-
</Box>
|
|
149
|
-
|
|
150
|
-
<Box
|
|
151
|
-
flexDirection="column"
|
|
152
|
-
paddingX={1}
|
|
153
|
-
overflow="scroll"
|
|
154
|
-
scrollTo={isSelected ? selectedCardIndex : undefined}
|
|
155
|
-
flexGrow={1}
|
|
156
|
-
>
|
|
157
|
-
{column.cards.map((card, cardIndex) => (
|
|
158
|
-
<CardComponent key={card.id} card={card} isSelected={isSelected && cardIndex === selectedCardIndex} />
|
|
159
|
-
))}
|
|
160
|
-
|
|
161
|
-
{column.cards.length === 0 && (
|
|
162
|
-
<Text dim italic>
|
|
163
|
-
No cards
|
|
164
|
-
</Text>
|
|
165
|
-
)}
|
|
166
|
-
</Box>
|
|
167
|
-
</Box>
|
|
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
|
-
</Box>
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ============================================================================
|
|
249
|
-
// Main
|
|
250
|
-
// ============================================================================
|
|
251
|
-
|
|
252
|
-
export async function main() {
|
|
253
|
-
using term = createTerm()
|
|
254
|
-
const { waitUntilExit } = await render(
|
|
255
|
-
<ExampleBanner meta={meta} controls="h/l column j/k card </> move Esc/q quit">
|
|
256
|
-
<KanbanBoard />
|
|
257
|
-
</ExampleBanner>,
|
|
258
|
-
term,
|
|
259
|
-
)
|
|
260
|
-
await waitUntilExit()
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (import.meta.main) {
|
|
264
|
-
await main()
|
|
265
|
-
}
|
package/apps/layout-ref.tsx
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
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 { render, Box, Text, H1, Kbd, Muted, useInput, useApp, createTerm, type BoxHandle, type Key } from "silvery"
|
|
12
|
-
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
13
|
-
|
|
14
|
-
export const meta: ExampleMeta = {
|
|
15
|
-
name: "Layout Ref",
|
|
16
|
-
description: "useBoxRect + useScrollRect for imperative layout measurement",
|
|
17
|
-
features: ["forwardRef", "BoxHandle", "onLayout", "getBoxRect()"],
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// ============================================================================
|
|
21
|
-
// Components
|
|
22
|
-
// ============================================================================
|
|
23
|
-
|
|
24
|
-
interface LayoutInfo {
|
|
25
|
-
x: number
|
|
26
|
-
y: number
|
|
27
|
-
width: number
|
|
28
|
-
height: number
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function ResizablePane({
|
|
32
|
-
title,
|
|
33
|
-
color,
|
|
34
|
-
onLayoutChange,
|
|
35
|
-
}: {
|
|
36
|
-
title: string
|
|
37
|
-
color: string
|
|
38
|
-
onLayoutChange: (info: LayoutInfo) => void
|
|
39
|
-
}) {
|
|
40
|
-
const boxRef = useRef<BoxHandle>(null)
|
|
41
|
-
|
|
42
|
-
// onLayout callback fires when this Box's dimensions change
|
|
43
|
-
return (
|
|
44
|
-
<Box
|
|
45
|
-
ref={boxRef}
|
|
46
|
-
flexGrow={1}
|
|
47
|
-
borderStyle="round"
|
|
48
|
-
borderColor={color}
|
|
49
|
-
padding={1}
|
|
50
|
-
onLayout={(layout) => onLayoutChange(layout)}
|
|
51
|
-
>
|
|
52
|
-
<H1 color={color}>{title}</H1>
|
|
53
|
-
</Box>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function ImperativeAccessDemo() {
|
|
58
|
-
const boxRef = useRef<BoxHandle>(null)
|
|
59
|
-
const [info, setInfo] = useState<string>("Click 'i' to inspect")
|
|
60
|
-
|
|
61
|
-
const inspect = () => {
|
|
62
|
-
if (!boxRef.current) {
|
|
63
|
-
setInfo("No ref attached")
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const content = boxRef.current.getBoxRect()
|
|
68
|
-
const screen = boxRef.current.getScrollRect()
|
|
69
|
-
const node = boxRef.current.getNode()
|
|
70
|
-
|
|
71
|
-
setInfo(
|
|
72
|
-
`Content: ${content?.width}x${content?.height} at (${content?.x},${content?.y})\n` +
|
|
73
|
-
`Screen: ${screen?.width}x${screen?.height} at (${screen?.x},${screen?.y})\n` +
|
|
74
|
-
`Node: ${node ? "available" : "null"}`,
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<Box ref={boxRef} flexDirection="column" borderStyle="double" borderColor="$accent" padding={1}>
|
|
80
|
-
<H1 color="$accent">Imperative Access (BoxHandle)</H1>
|
|
81
|
-
<Muted>Press 'i' to inspect this box</Muted>
|
|
82
|
-
<Box marginTop={1}>
|
|
83
|
-
<Text>{info}</Text>
|
|
84
|
-
</Box>
|
|
85
|
-
{/* Expose inspect function via closure */}
|
|
86
|
-
<InspectTrigger onInspect={inspect} />
|
|
87
|
-
</Box>
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Hidden component to trigger inspect on keypress
|
|
92
|
-
function InspectTrigger({ onInspect }: { onInspect: () => void }) {
|
|
93
|
-
useInput((input: string) => {
|
|
94
|
-
if (input === "i") {
|
|
95
|
-
onInspect()
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
return null
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function LayoutRefApp() {
|
|
102
|
-
const { exit } = useApp()
|
|
103
|
-
const [layouts, setLayouts] = useState<Record<string, LayoutInfo>>({})
|
|
104
|
-
|
|
105
|
-
useInput((input: string, key: Key) => {
|
|
106
|
-
if (input === "q" || key.escape) {
|
|
107
|
-
exit()
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
const handleLayoutChange = (pane: string) => (info: LayoutInfo) => {
|
|
112
|
-
setLayouts((prev) => ({ ...prev, [pane]: info }))
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<Box flexDirection="column" padding={1}>
|
|
117
|
-
{/* Row of resizable panes with onLayout callbacks */}
|
|
118
|
-
<Box flexDirection="row" gap={1} height={8}>
|
|
119
|
-
<ResizablePane title="Pane A" color="$success" onLayoutChange={handleLayoutChange("a")} />
|
|
120
|
-
<ResizablePane title="Pane B" color="$info" onLayoutChange={handleLayoutChange("b")} />
|
|
121
|
-
<ResizablePane title="Pane C" color="$primary" onLayoutChange={handleLayoutChange("c")} />
|
|
122
|
-
</Box>
|
|
123
|
-
|
|
124
|
-
{/* Show layout info from onLayout callbacks */}
|
|
125
|
-
<Box marginTop={1} borderStyle="single" borderColor="$border" padding={1}>
|
|
126
|
-
<Box flexDirection="column">
|
|
127
|
-
<Text bold dim>
|
|
128
|
-
onLayout Results:
|
|
129
|
-
</Text>
|
|
130
|
-
{Object.entries(layouts).map(([pane, info]) => (
|
|
131
|
-
<Text key={pane} dim>
|
|
132
|
-
Pane {pane.toUpperCase()}: {info.width}x{info.height} at ({info.x},{info.y})
|
|
133
|
-
</Text>
|
|
134
|
-
))}
|
|
135
|
-
{Object.keys(layouts).length === 0 && (
|
|
136
|
-
<Text dim italic>
|
|
137
|
-
Waiting for layout...
|
|
138
|
-
</Text>
|
|
139
|
-
)}
|
|
140
|
-
</Box>
|
|
141
|
-
</Box>
|
|
142
|
-
|
|
143
|
-
{/* Imperative access demo */}
|
|
144
|
-
<Box flexGrow={1} marginTop={1}>
|
|
145
|
-
<ImperativeAccessDemo />
|
|
146
|
-
</Box>
|
|
147
|
-
|
|
148
|
-
<Muted>
|
|
149
|
-
{" "}
|
|
150
|
-
<Kbd>i</Kbd> inspect <Kbd>Esc/q</Kbd> quit
|
|
151
|
-
</Muted>
|
|
152
|
-
</Box>
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ============================================================================
|
|
157
|
-
// Main
|
|
158
|
-
// ============================================================================
|
|
159
|
-
|
|
160
|
-
export async function main() {
|
|
161
|
-
using term = createTerm()
|
|
162
|
-
const { waitUntilExit } = await render(
|
|
163
|
-
<ExampleBanner meta={meta} controls="i inspect Esc/q quit">
|
|
164
|
-
<LayoutRefApp />
|
|
165
|
-
</ExampleBanner>,
|
|
166
|
-
term,
|
|
167
|
-
)
|
|
168
|
-
await waitUntilExit()
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (import.meta.main) {
|
|
172
|
-
await main()
|
|
173
|
-
}
|