@silvery/examples 0.5.2 → 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.
- package/LICENSE +21 -0
- package/{examples/apps → apps}/aichat/index.tsx +4 -3
- package/{examples/apps → apps}/async-data.tsx +4 -4
- package/apps/components.tsx +658 -0
- package/{examples/apps → apps}/data-explorer.tsx +8 -8
- package/{examples/apps → apps}/dev-tools.tsx +35 -19
- package/{examples/apps → apps}/inline-bench.tsx +3 -1
- package/{examples/apps → apps}/kanban.tsx +20 -22
- package/{examples/apps → apps}/layout-ref.tsx +6 -6
- package/{examples/apps → apps}/panes/index.tsx +1 -1
- package/{examples/apps → apps}/paste-demo.tsx +2 -2
- package/{examples/apps → apps}/scroll.tsx +2 -2
- package/{examples/apps → apps}/search-filter.tsx +1 -1
- package/apps/selection.tsx +342 -0
- package/apps/spatial-focus-demo.tsx +368 -0
- package/{examples/apps → apps}/task-list.tsx +1 -1
- package/apps/terminal-caps-demo.tsx +334 -0
- package/apps/text-selection-demo.tsx +189 -0
- package/apps/textarea.tsx +155 -0
- package/{examples/apps → apps}/theme.tsx +1 -1
- package/apps/vterm-demo/index.tsx +216 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +190 -0
- package/dist/cli.mjs.map +1 -0
- package/layout/dashboard.tsx +953 -0
- package/layout/live-resize.tsx +282 -0
- package/layout/overflow.tsx +51 -0
- package/layout/text-layout.tsx +283 -0
- package/package.json +27 -11
- package/bin/cli.ts +0 -294
- package/examples/apps/components.tsx +0 -463
- package/examples/apps/textarea.tsx +0 -91
- /package/{examples/_banner.tsx → _banner.tsx} +0 -0
- /package/{examples/apps → apps}/aichat/components.tsx +0 -0
- /package/{examples/apps → apps}/aichat/script.ts +0 -0
- /package/{examples/apps → apps}/aichat/state.ts +0 -0
- /package/{examples/apps → apps}/aichat/types.ts +0 -0
- /package/{examples/apps → apps}/app-todo.tsx +0 -0
- /package/{examples/apps → apps}/cli-wizard.tsx +0 -0
- /package/{examples/apps → apps}/clipboard.tsx +0 -0
- /package/{examples/apps → apps}/explorer.tsx +0 -0
- /package/{examples/apps → apps}/gallery.tsx +0 -0
- /package/{examples/apps → apps}/outline.tsx +0 -0
- /package/{examples/apps → apps}/terminal.tsx +0 -0
- /package/{examples/apps → apps}/transform.tsx +0 -0
- /package/{examples/apps → apps}/virtual-10k.tsx +0 -0
- /package/{examples/components → components}/counter.tsx +0 -0
- /package/{examples/components → components}/hello.tsx +0 -0
- /package/{examples/components → components}/progress-bar.tsx +0 -0
- /package/{examples/components → components}/select-list.tsx +0 -0
- /package/{examples/components → components}/spinner.tsx +0 -0
- /package/{examples/components → components}/text-input.tsx +0 -0
- /package/{examples/components → components}/virtual-list.tsx +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Resize Demo
|
|
3
|
+
*
|
|
4
|
+
* THE showcase demo for silvery's unique capability: components that know their size.
|
|
5
|
+
*
|
|
6
|
+
* Demonstrates:
|
|
7
|
+
* - useBoxRect() providing real-time width/height during render
|
|
8
|
+
* - Multi-column layout that reflows from 1 to 2 to 3 columns based on width
|
|
9
|
+
* - Responsive breakpoints with visual feedback
|
|
10
|
+
* - Content that adapts its presentation based on available space
|
|
11
|
+
* - No useEffect, no layout thrashing — dimensions are synchronous
|
|
12
|
+
*
|
|
13
|
+
* Usage: bun run examples/live-resize/index.tsx
|
|
14
|
+
*
|
|
15
|
+
* Try resizing your terminal to see the layout reflow in real-time!
|
|
16
|
+
*
|
|
17
|
+
* Controls:
|
|
18
|
+
* Esc/q or Ctrl+C - Quit
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import React from "react"
|
|
22
|
+
import { Box, Text, H1, H3, Kbd, Muted, Small, useBoxRect } from "silvery"
|
|
23
|
+
import { run, useInput, type Key } from "silvery/runtime"
|
|
24
|
+
import { useCallback } from "react"
|
|
25
|
+
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
26
|
+
|
|
27
|
+
export const meta: ExampleMeta = {
|
|
28
|
+
name: "Live Resize",
|
|
29
|
+
description: "Responsive multi-column grid that reflows based on terminal width",
|
|
30
|
+
features: ["useBoxRect()", "responsive breakpoints", "Box flexDirection"],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Types
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
interface CardData {
|
|
38
|
+
title: string
|
|
39
|
+
icon: string
|
|
40
|
+
value: string
|
|
41
|
+
detail: string
|
|
42
|
+
color: string
|
|
43
|
+
sparkline: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Data
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
const CARDS: CardData[] = [
|
|
51
|
+
{
|
|
52
|
+
title: "CPU Usage",
|
|
53
|
+
icon: "\u{1f4bb}",
|
|
54
|
+
value: "42%",
|
|
55
|
+
detail: "4 cores, 2.4 GHz base",
|
|
56
|
+
color: "green",
|
|
57
|
+
sparkline: "\u2582\u2583\u2585\u2587\u2586\u2584\u2583\u2585\u2587\u2588\u2586\u2584\u2583\u2582\u2583\u2585",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
title: "Memory",
|
|
61
|
+
icon: "\u{1f9e0}",
|
|
62
|
+
value: "8.2 GB",
|
|
63
|
+
detail: "of 16 GB (51% used)",
|
|
64
|
+
color: "cyan",
|
|
65
|
+
sparkline: "\u2584\u2584\u2585\u2585\u2585\u2586\u2586\u2586\u2585\u2585\u2586\u2586\u2587\u2587\u2586\u2586",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
title: "Disk I/O",
|
|
69
|
+
icon: "\u{1f4be}",
|
|
70
|
+
value: "234 MB/s",
|
|
71
|
+
detail: "Read: 180 MB/s Write: 54 MB/s",
|
|
72
|
+
color: "yellow",
|
|
73
|
+
sparkline: "\u2581\u2582\u2583\u2587\u2588\u2587\u2584\u2582\u2581\u2582\u2585\u2587\u2586\u2583\u2582\u2581",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
title: "Network",
|
|
77
|
+
icon: "\u{1f310}",
|
|
78
|
+
value: "1.2 Gb/s",
|
|
79
|
+
detail: "In: 800 Mb/s Out: 400 Mb/s",
|
|
80
|
+
color: "magenta",
|
|
81
|
+
sparkline: "\u2583\u2584\u2585\u2586\u2587\u2586\u2585\u2584\u2585\u2586\u2587\u2588\u2587\u2586\u2585\u2584",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
title: "Processes",
|
|
85
|
+
icon: "\u{2699}\u{fe0f}",
|
|
86
|
+
value: "247",
|
|
87
|
+
detail: "12 running, 235 sleeping",
|
|
88
|
+
color: "blue",
|
|
89
|
+
sparkline: "\u2585\u2585\u2585\u2586\u2585\u2585\u2585\u2585\u2586\u2585\u2585\u2585\u2586\u2585\u2585\u2585",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
title: "Temperature",
|
|
93
|
+
icon: "\u{1f321}\u{fe0f}",
|
|
94
|
+
value: "62 C",
|
|
95
|
+
detail: "Max: 85 C (safe range)",
|
|
96
|
+
color: "red",
|
|
97
|
+
sparkline: "\u2583\u2583\u2584\u2584\u2585\u2585\u2586\u2586\u2585\u2585\u2584\u2584\u2583\u2584\u2585\u2585",
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// Components
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
function MetricCard({ card, compact }: { card: CardData; compact: boolean }) {
|
|
106
|
+
if (compact) {
|
|
107
|
+
// Minimal: single-line card for narrow terminals
|
|
108
|
+
return (
|
|
109
|
+
<Box borderStyle="round" borderColor={card.color} paddingX={1} flexDirection="row" justifyContent="space-between">
|
|
110
|
+
<H1 color={card.color}>{card.title}</H1>
|
|
111
|
+
<H3>{card.value}</H3>
|
|
112
|
+
</Box>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Full card with sparkline and details
|
|
117
|
+
return (
|
|
118
|
+
<Box borderStyle="round" borderColor={card.color} paddingX={1} flexDirection="column" flexGrow={1}>
|
|
119
|
+
<Box justifyContent="space-between">
|
|
120
|
+
<H1 color={card.color}>{card.title}</H1>
|
|
121
|
+
<H1 color={card.color}>{card.value}</H1>
|
|
122
|
+
</Box>
|
|
123
|
+
<Text color={card.color}>{card.sparkline}</Text>
|
|
124
|
+
<Small>{card.detail}</Small>
|
|
125
|
+
</Box>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function BreakpointIndicator({ width, columns }: { width: number; columns: number }) {
|
|
130
|
+
const breakpoints = [
|
|
131
|
+
{ threshold: 0, cols: 1, label: "< 60" },
|
|
132
|
+
{ threshold: 60, cols: 2, label: "60-99" },
|
|
133
|
+
{ threshold: 100, cols: 3, label: "100+" },
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Box gap={2} paddingX={1}>
|
|
138
|
+
{breakpoints.map((bp) => {
|
|
139
|
+
const isActive = bp.cols === columns
|
|
140
|
+
return (
|
|
141
|
+
<Box key={bp.cols} gap={1}>
|
|
142
|
+
<Text color={isActive ? "green" : "gray"} bold={isActive}>
|
|
143
|
+
{isActive ? "\u25cf" : "\u25cb"}
|
|
144
|
+
</Text>
|
|
145
|
+
<Text color={isActive ? "white" : "gray"} bold={isActive}>
|
|
146
|
+
{bp.cols} col{bp.cols > 1 ? "s" : " "} ({bp.label})
|
|
147
|
+
</Text>
|
|
148
|
+
</Box>
|
|
149
|
+
)
|
|
150
|
+
})}
|
|
151
|
+
</Box>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function GridLayout({ cards, columns, compact }: { cards: CardData[]; columns: number; compact: boolean }) {
|
|
156
|
+
if (columns === 1) {
|
|
157
|
+
return (
|
|
158
|
+
<Box flexDirection="column" gap={compact ? 0 : 1} flexGrow={1}>
|
|
159
|
+
{cards.map((card) => (
|
|
160
|
+
<MetricCard key={card.title} card={card} compact={compact} />
|
|
161
|
+
))}
|
|
162
|
+
</Box>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Build rows of N columns
|
|
167
|
+
const rows: CardData[][] = []
|
|
168
|
+
for (let i = 0; i < cards.length; i += columns) {
|
|
169
|
+
rows.push(cards.slice(i, i + columns))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<Box flexDirection="column" gap={1} flexGrow={1}>
|
|
174
|
+
{rows.map((row, rowIndex) => (
|
|
175
|
+
<Box key={rowIndex} flexDirection="row" gap={1}>
|
|
176
|
+
{row.map((card) => (
|
|
177
|
+
<Box key={card.title} flexGrow={1} flexBasis={0}>
|
|
178
|
+
<MetricCard card={card} compact={false} />
|
|
179
|
+
</Box>
|
|
180
|
+
))}
|
|
181
|
+
{/* Fill remaining slots for even spacing */}
|
|
182
|
+
{row.length < columns &&
|
|
183
|
+
Array.from({ length: columns - row.length }, (_, i) => (
|
|
184
|
+
<Box key={`spacer-${i}`} flexGrow={1} flexBasis={0} />
|
|
185
|
+
))}
|
|
186
|
+
</Box>
|
|
187
|
+
))}
|
|
188
|
+
</Box>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function CodeSnippet({ width }: { width: number }) {
|
|
193
|
+
const showSnippet = width >= 60
|
|
194
|
+
|
|
195
|
+
if (!showSnippet) {
|
|
196
|
+
return (
|
|
197
|
+
<Box paddingX={1}>
|
|
198
|
+
<Text dim italic>
|
|
199
|
+
(Widen terminal to see the code that powers this)
|
|
200
|
+
</Text>
|
|
201
|
+
</Box>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<Box flexDirection="column" borderStyle="single" borderColor="$border" paddingX={1}>
|
|
207
|
+
<H1 color="yellow">How it works:</H1>
|
|
208
|
+
<Text color="gray">
|
|
209
|
+
{" "}
|
|
210
|
+
<Text color="magenta">const</Text> {"{"} width {"}"} = <Text color="cyan">useBoxRect</Text>()
|
|
211
|
+
</Text>
|
|
212
|
+
<Text color="gray">
|
|
213
|
+
{" "}
|
|
214
|
+
<Text color="magenta">const</Text> columns = width {">"} 100 ? <Text color="green">3</Text> : width {">"} 60 ?{" "}
|
|
215
|
+
<Text color="green">2</Text> : <Text color="green">1</Text>
|
|
216
|
+
</Text>
|
|
217
|
+
<Text dim italic>
|
|
218
|
+
{" "}// No useEffect, no layout thrashing. Synchronous.
|
|
219
|
+
</Text>
|
|
220
|
+
</Box>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// Main App
|
|
226
|
+
// ============================================================================
|
|
227
|
+
|
|
228
|
+
function LiveResize() {
|
|
229
|
+
const { width, height } = useBoxRect()
|
|
230
|
+
|
|
231
|
+
// Responsive breakpoints
|
|
232
|
+
const columns = width >= 100 ? 3 : width >= 60 ? 2 : 1
|
|
233
|
+
const compact = height < 20 || width < 40
|
|
234
|
+
|
|
235
|
+
useInput(
|
|
236
|
+
useCallback((input: string, key: Key) => {
|
|
237
|
+
if (input === "q" || key.escape || (key.ctrl && input === "c")) {
|
|
238
|
+
return "exit"
|
|
239
|
+
}
|
|
240
|
+
}, []),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<Box flexDirection="column" width="100%" height="100%" padding={1}>
|
|
245
|
+
{/* Breakpoint indicator */}
|
|
246
|
+
<BreakpointIndicator width={width} columns={columns} />
|
|
247
|
+
|
|
248
|
+
{/* Main grid */}
|
|
249
|
+
<Box flexGrow={1} flexDirection="column" marginTop={1}>
|
|
250
|
+
<GridLayout cards={CARDS} columns={columns} compact={compact} />
|
|
251
|
+
</Box>
|
|
252
|
+
|
|
253
|
+
{/* Code snippet showing how it works */}
|
|
254
|
+
{!compact && <CodeSnippet width={width} />}
|
|
255
|
+
|
|
256
|
+
{/* Footer */}
|
|
257
|
+
<Box justifyContent="space-between" paddingX={1}>
|
|
258
|
+
<Muted>Resize your terminal to see the layout reflow</Muted>
|
|
259
|
+
<Muted>
|
|
260
|
+
<Kbd>Esc/q</Kbd> quit
|
|
261
|
+
</Muted>
|
|
262
|
+
</Box>
|
|
263
|
+
</Box>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ============================================================================
|
|
268
|
+
// Main
|
|
269
|
+
// ============================================================================
|
|
270
|
+
|
|
271
|
+
async function main() {
|
|
272
|
+
const handle = await run(
|
|
273
|
+
<ExampleBanner meta={meta} controls="Resize terminal to see reflow Esc/q quit">
|
|
274
|
+
<LiveResize />
|
|
275
|
+
</ExampleBanner>,
|
|
276
|
+
)
|
|
277
|
+
await handle.waitUntilExit()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (import.meta.main) {
|
|
281
|
+
main().catch(console.error)
|
|
282
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { render, Box, Text, useApp, useInput, createTerm } from "silvery"
|
|
3
|
+
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
4
|
+
|
|
5
|
+
export const meta: ExampleMeta = {
|
|
6
|
+
name: "Overflow",
|
|
7
|
+
description: 'overflow="hidden" content clipping demonstration',
|
|
8
|
+
features: ['overflow="hidden"', "Box height"],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function OverflowApp() {
|
|
12
|
+
const { exit } = useApp()
|
|
13
|
+
useInput((input, key) => {
|
|
14
|
+
if (input === "q" || key.escape) exit()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box flexDirection="column" padding={1}>
|
|
19
|
+
<Text color="yellow">Title</Text>
|
|
20
|
+
|
|
21
|
+
<Box borderStyle="single" borderColor="$primary" height={5} overflow="hidden">
|
|
22
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
23
|
+
<Text>Line 1</Text>
|
|
24
|
+
<Text>Line 2</Text>
|
|
25
|
+
<Text>Line 3</Text>
|
|
26
|
+
<Text>Line 4</Text>
|
|
27
|
+
<Text>Line 5</Text>
|
|
28
|
+
<Text>Line 6 - should NOT appear</Text>
|
|
29
|
+
<Text>Line 7 - should NOT appear</Text>
|
|
30
|
+
</Box>
|
|
31
|
+
</Box>
|
|
32
|
+
|
|
33
|
+
<Text color="$success">This should NOT be corrupted</Text>
|
|
34
|
+
</Box>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
using term = createTerm()
|
|
40
|
+
const { waitUntilExit } = await render(
|
|
41
|
+
<ExampleBanner meta={meta} controls="Esc/q quit">
|
|
42
|
+
<OverflowApp />
|
|
43
|
+
</ExampleBanner>,
|
|
44
|
+
term,
|
|
45
|
+
)
|
|
46
|
+
await waitUntilExit()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (import.meta.main) {
|
|
50
|
+
main().catch(console.error)
|
|
51
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pretext Demo — snug-content bubbles + even wrapping comparison
|
|
3
|
+
*
|
|
4
|
+
* Interactive demo showcasing Silvery's Pretext-inspired text layout:
|
|
5
|
+
* - width="snug-content" — tightest box width for same line count (shrinkwrap)
|
|
6
|
+
* - wrap="even" — minimum-raggedness line breaking (Knuth-Plass)
|
|
7
|
+
*
|
|
8
|
+
* Inspired by https://chenglou.me/pretext/
|
|
9
|
+
*
|
|
10
|
+
* Usage: bun examples/pretext-demo.tsx
|
|
11
|
+
*
|
|
12
|
+
* Controls:
|
|
13
|
+
* j/k - Cycle demo sections
|
|
14
|
+
* Esc/q - Quit
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import React, { useState, useCallback } from "react"
|
|
18
|
+
import { Box, Text, H2, Muted, Small, Kbd, Divider } from "silvery"
|
|
19
|
+
import { run, useInput, type Key } from "silvery/runtime"
|
|
20
|
+
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
21
|
+
|
|
22
|
+
export const meta: ExampleMeta = {
|
|
23
|
+
name: "text layout",
|
|
24
|
+
description: "Snug-content bubbles + even wrapping — inspired by chenglou/pretext",
|
|
25
|
+
demo: true,
|
|
26
|
+
features: ['width="snug-content"', 'wrap="even"', "chat bubbles", "paragraph layout"],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Sample Data
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
const CHAT_MESSAGES = [
|
|
34
|
+
{ sender: "Alice", text: "Hey!" },
|
|
35
|
+
{ sender: "Bob", text: "What are you working on?" },
|
|
36
|
+
{ sender: "Alice", text: "Building a terminal UI framework with beautiful text layout." },
|
|
37
|
+
{
|
|
38
|
+
sender: "Bob",
|
|
39
|
+
text: "That sounds interesting. Does it handle word wrapping well? Most terminal apps have really ugly ragged text.",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
sender: "Alice",
|
|
43
|
+
text: "Yes! It uses Pretext-inspired algorithms for snug bubbles and even line breaking.",
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const PARAGRAPH =
|
|
48
|
+
"The quick brown fox jumps over the lazy dog. " +
|
|
49
|
+
"Typography in terminal applications has always been limited by the character grid, " +
|
|
50
|
+
"but modern algorithms can distribute text across lines for minimum raggedness, " +
|
|
51
|
+
"producing results that rival print-quality typesetting. " +
|
|
52
|
+
"Silvery brings these techniques to the terminal."
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Components
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
/** A single chat bubble with configurable width and wrap mode. */
|
|
59
|
+
function Bubble({
|
|
60
|
+
sender,
|
|
61
|
+
text,
|
|
62
|
+
width,
|
|
63
|
+
wrap,
|
|
64
|
+
align,
|
|
65
|
+
}: {
|
|
66
|
+
sender: string
|
|
67
|
+
text: string
|
|
68
|
+
width: "fit-content" | "snug-content"
|
|
69
|
+
wrap?: "wrap" | "even"
|
|
70
|
+
align?: "flex-start" | "flex-end"
|
|
71
|
+
}) {
|
|
72
|
+
return (
|
|
73
|
+
<Box flexDirection="column" alignItems={align ?? "flex-start"}>
|
|
74
|
+
<Small> {sender}</Small>
|
|
75
|
+
<Box width={width} borderStyle="round" borderColor="$border" paddingX={1} maxWidth={48}>
|
|
76
|
+
<Text wrap={wrap ?? "wrap"}>{text}</Text>
|
|
77
|
+
</Box>
|
|
78
|
+
</Box>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Column of chat bubbles with a label. */
|
|
83
|
+
function BubbleColumn({
|
|
84
|
+
label,
|
|
85
|
+
sublabel,
|
|
86
|
+
width,
|
|
87
|
+
wrap,
|
|
88
|
+
}: {
|
|
89
|
+
label: string
|
|
90
|
+
sublabel: string
|
|
91
|
+
width: "fit-content" | "snug-content"
|
|
92
|
+
wrap?: "wrap" | "even"
|
|
93
|
+
}) {
|
|
94
|
+
return (
|
|
95
|
+
<Box flexDirection="column" flexGrow={1} flexBasis={0}>
|
|
96
|
+
<Text bold color="$accent">
|
|
97
|
+
{label}
|
|
98
|
+
</Text>
|
|
99
|
+
<Muted>{sublabel}</Muted>
|
|
100
|
+
<Text> </Text>
|
|
101
|
+
<Box flexDirection="column" gap={1}>
|
|
102
|
+
{CHAT_MESSAGES.map((msg, i) => (
|
|
103
|
+
<Bubble
|
|
104
|
+
key={i}
|
|
105
|
+
sender={msg.sender}
|
|
106
|
+
text={msg.text}
|
|
107
|
+
width={width}
|
|
108
|
+
wrap={wrap}
|
|
109
|
+
align={msg.sender === "Bob" ? "flex-end" : "flex-start"}
|
|
110
|
+
/>
|
|
111
|
+
))}
|
|
112
|
+
</Box>
|
|
113
|
+
</Box>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Side-by-side paragraph comparison. */
|
|
118
|
+
function ParagraphComparison({ label, sublabel, wrap }: { label: string; sublabel: string; wrap: "wrap" | "even" }) {
|
|
119
|
+
return (
|
|
120
|
+
<Box flexDirection="column" flexGrow={1} flexBasis={0}>
|
|
121
|
+
<Text bold color="$accent">
|
|
122
|
+
{label}
|
|
123
|
+
</Text>
|
|
124
|
+
<Muted>{sublabel}</Muted>
|
|
125
|
+
<Text> </Text>
|
|
126
|
+
<Box width={52} borderStyle="single" borderColor="$border" paddingX={1}>
|
|
127
|
+
<Text wrap={wrap}>{PARAGRAPH}</Text>
|
|
128
|
+
</Box>
|
|
129
|
+
</Box>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Demo Sections
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
function Demo1Bubbles() {
|
|
138
|
+
return (
|
|
139
|
+
<Box flexDirection="column">
|
|
140
|
+
<H2>Chat Bubbles: fit-content vs snug-content</H2>
|
|
141
|
+
<Muted>{" "}fit-content sizes to the widest wrapped line (dead space on short lines).</Muted>
|
|
142
|
+
<Muted>{" "}snug-content binary-searches for the tightest width with the same line count.</Muted>
|
|
143
|
+
<Text> </Text>
|
|
144
|
+
<Box flexDirection="row" gap={3} paddingX={1}>
|
|
145
|
+
<BubbleColumn label='width="fit-content"' sublabel="CSS default — dead space" width="fit-content" />
|
|
146
|
+
<BubbleColumn label='width="snug-content"' sublabel="Pretext shrinkwrap — tight" width="snug-content" />
|
|
147
|
+
</Box>
|
|
148
|
+
</Box>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function Demo2EvenWrap() {
|
|
153
|
+
return (
|
|
154
|
+
<Box flexDirection="column">
|
|
155
|
+
<H2>Paragraph Layout: greedy vs even wrapping</H2>
|
|
156
|
+
<Muted>{" "}Greedy fills each line left-to-right, leaving a ragged right edge.</Muted>
|
|
157
|
+
<Muted>{" "}Even uses minimum-raggedness DP to distribute words across all lines.</Muted>
|
|
158
|
+
<Text> </Text>
|
|
159
|
+
<Box flexDirection="row" gap={3} paddingX={1}>
|
|
160
|
+
<ParagraphComparison label='wrap="wrap"' sublabel="Greedy — ragged right edge" wrap="wrap" />
|
|
161
|
+
<ParagraphComparison label='wrap="even"' sublabel="Min-raggedness — balanced lines" wrap="even" />
|
|
162
|
+
</Box>
|
|
163
|
+
</Box>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function Demo3Combined() {
|
|
168
|
+
return (
|
|
169
|
+
<Box flexDirection="column">
|
|
170
|
+
<H2>Combined: snug-content + even wrapping</H2>
|
|
171
|
+
<Muted>{" "}The tightest, most beautiful text layout — both features together.</Muted>
|
|
172
|
+
<Text> </Text>
|
|
173
|
+
<Box flexDirection="row" gap={3} paddingX={1}>
|
|
174
|
+
<Box flexDirection="column" flexGrow={1} flexBasis={0}>
|
|
175
|
+
<Text bold color="$accent">
|
|
176
|
+
Default (fit-content + greedy)
|
|
177
|
+
</Text>
|
|
178
|
+
<Muted>Widest line sets width, lines fill greedily</Muted>
|
|
179
|
+
<Text> </Text>
|
|
180
|
+
<Box flexDirection="column" gap={1}>
|
|
181
|
+
<Box width="fit-content" borderStyle="round" borderColor="$border" paddingX={1} maxWidth={48}>
|
|
182
|
+
<Text wrap="wrap">
|
|
183
|
+
Typography in terminal applications has always been limited by the character grid, but modern algorithms
|
|
184
|
+
change that.
|
|
185
|
+
</Text>
|
|
186
|
+
</Box>
|
|
187
|
+
<Box width="fit-content" borderStyle="round" borderColor="$border" paddingX={1} maxWidth={48}>
|
|
188
|
+
<Text wrap="wrap">Silvery brings Pretext-inspired layout to the terminal with two simple props.</Text>
|
|
189
|
+
</Box>
|
|
190
|
+
</Box>
|
|
191
|
+
</Box>
|
|
192
|
+
<Box flexDirection="column" flexGrow={1} flexBasis={0}>
|
|
193
|
+
<Text bold color="$accent">
|
|
194
|
+
Pretext (snug-content + even)
|
|
195
|
+
</Text>
|
|
196
|
+
<Muted>Tightest width, balanced line lengths</Muted>
|
|
197
|
+
<Text> </Text>
|
|
198
|
+
<Box flexDirection="column" gap={1}>
|
|
199
|
+
<Box width="snug-content" borderStyle="round" borderColor="$primary" paddingX={1} maxWidth={48}>
|
|
200
|
+
<Text wrap="even">
|
|
201
|
+
Typography in terminal applications has always been limited by the character grid, but modern algorithms
|
|
202
|
+
change that.
|
|
203
|
+
</Text>
|
|
204
|
+
</Box>
|
|
205
|
+
<Box width="snug-content" borderStyle="round" borderColor="$primary" paddingX={1} maxWidth={48}>
|
|
206
|
+
<Text wrap="even">Silvery brings Pretext-inspired layout to the terminal with two simple props.</Text>
|
|
207
|
+
</Box>
|
|
208
|
+
</Box>
|
|
209
|
+
</Box>
|
|
210
|
+
</Box>
|
|
211
|
+
</Box>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// Main App
|
|
217
|
+
// ============================================================================
|
|
218
|
+
|
|
219
|
+
const DEMOS = [Demo1Bubbles, Demo2EvenWrap, Demo3Combined]
|
|
220
|
+
const DEMO_LABELS = ["Chat Bubbles", "Even Wrapping", "Combined"]
|
|
221
|
+
|
|
222
|
+
function PretextDemo() {
|
|
223
|
+
const [demoIndex, setDemoIndex] = useState(0)
|
|
224
|
+
|
|
225
|
+
useInput(
|
|
226
|
+
useCallback((input: string, key: Key) => {
|
|
227
|
+
if (input === "q" || key.escape) return "exit"
|
|
228
|
+
if (input === "j" || key.downArrow || key.rightArrow) {
|
|
229
|
+
setDemoIndex((i) => Math.min(i + 1, DEMOS.length - 1))
|
|
230
|
+
}
|
|
231
|
+
if (input === "k" || key.upArrow || key.leftArrow) {
|
|
232
|
+
setDemoIndex((i) => Math.max(i - 1, 0))
|
|
233
|
+
}
|
|
234
|
+
if (input === "1") setDemoIndex(0)
|
|
235
|
+
if (input === "2") setDemoIndex(1)
|
|
236
|
+
if (input === "3") setDemoIndex(2)
|
|
237
|
+
}, []),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
const Demo = DEMOS[demoIndex]!
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<Box flexDirection="column" padding={1} gap={1}>
|
|
244
|
+
{/* Tab bar */}
|
|
245
|
+
<Box gap={2}>
|
|
246
|
+
{DEMO_LABELS.map((label, i) => (
|
|
247
|
+
<Text key={i} bold={i === demoIndex} color={i === demoIndex ? "$primary" : "$muted"}>
|
|
248
|
+
{i === demoIndex ? "▸ " : " "}
|
|
249
|
+
{i + 1}. {label}
|
|
250
|
+
</Text>
|
|
251
|
+
))}
|
|
252
|
+
</Box>
|
|
253
|
+
<Divider />
|
|
254
|
+
{/* Active demo */}
|
|
255
|
+
<Demo />
|
|
256
|
+
{/* Footer */}
|
|
257
|
+
<Box>
|
|
258
|
+
<Muted>
|
|
259
|
+
{" "}
|
|
260
|
+
<Kbd>j/k</Kbd> or <Kbd>1-3</Kbd> switch demo{" "}
|
|
261
|
+
<Kbd>Esc/q</Kbd> quit
|
|
262
|
+
</Muted>
|
|
263
|
+
</Box>
|
|
264
|
+
</Box>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// Entry Point
|
|
270
|
+
// ============================================================================
|
|
271
|
+
|
|
272
|
+
async function main() {
|
|
273
|
+
const handle = await run(
|
|
274
|
+
<ExampleBanner meta={meta} controls="j/k switch demo 1-3 jump Esc/q quit">
|
|
275
|
+
<PretextDemo />
|
|
276
|
+
</ExampleBanner>,
|
|
277
|
+
)
|
|
278
|
+
await handle.waitUntilExit()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (import.meta.main) {
|
|
282
|
+
main().catch(console.error)
|
|
283
|
+
}
|
package/package.json
CHANGED
|
@@ -1,32 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silvery/examples",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Example apps and component demos for silvery —
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "Example apps and component demos for silvery — npx @silvery/examples <name>",
|
|
5
5
|
"license": "MIT",
|
|
6
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": "
|
|
10
|
+
"directory": "examples"
|
|
11
11
|
},
|
|
12
|
+
"type": "module",
|
|
12
13
|
"bin": {
|
|
13
|
-
"silvery-examples": "./
|
|
14
|
+
"silvery-examples": "./dist/cli.mjs"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
|
-
"
|
|
17
|
-
"
|
|
17
|
+
"dist",
|
|
18
|
+
"components",
|
|
19
|
+
"apps",
|
|
20
|
+
"layout",
|
|
21
|
+
"runtime",
|
|
22
|
+
"inline",
|
|
23
|
+
"kitty",
|
|
24
|
+
"_banner.tsx"
|
|
18
25
|
],
|
|
19
|
-
"
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/cli.d.mts",
|
|
29
|
+
"import": "./dist/cli.mjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
20
32
|
"publishConfig": {
|
|
21
33
|
"access": "public"
|
|
22
34
|
},
|
|
35
|
+
"tsdown": {
|
|
36
|
+
"entry": "bin/cli.ts",
|
|
37
|
+
"format": "esm",
|
|
38
|
+
"dts": true,
|
|
39
|
+
"clean": true
|
|
40
|
+
},
|
|
23
41
|
"dependencies": {
|
|
24
|
-
"
|
|
25
|
-
"@silvery/create": "0.5.0",
|
|
26
|
-
"silvery": "0.11.1"
|
|
42
|
+
"silvery": "0.17.1"
|
|
27
43
|
},
|
|
28
44
|
"engines": {
|
|
29
45
|
"bun": ">=1.0",
|
|
30
46
|
"node": ">=23.6.0"
|
|
31
47
|
}
|
|
32
|
-
}
|
|
48
|
+
}
|