silvery 0.3.0 → 0.4.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 +41 -145
- package/dist/chalk.js +3 -0
- package/dist/chalk.js.map +11 -0
- package/dist/index.js +340 -0
- package/dist/index.js.map +282 -0
- package/dist/ink.js +129 -0
- package/dist/ink.js.map +140 -0
- package/dist/runtime.js +394 -0
- package/dist/runtime.js.map +286 -0
- package/dist/theme.js +343 -0
- package/dist/theme.js.map +286 -0
- package/dist/ui/animation.js +3 -0
- package/dist/ui/animation.js.map +15 -0
- package/dist/ui/ansi.js +3 -0
- package/dist/ui/ansi.js.map +10 -0
- package/dist/ui/cli.js +8 -0
- package/dist/ui/cli.js.map +14 -0
- package/dist/ui/display.js +4 -0
- package/dist/ui/display.js.map +10 -0
- package/dist/ui/image.js +4 -0
- package/dist/ui/image.js.map +15 -0
- package/dist/ui/input.js +3 -0
- package/dist/ui/input.js.map +11 -0
- package/dist/ui/progress.js +8 -0
- package/dist/ui/progress.js.map +20 -0
- package/dist/ui/react.js +3 -0
- package/dist/ui/react.js.map +15 -0
- package/dist/ui/utils.js +3 -0
- package/dist/ui/utils.js.map +10 -0
- package/dist/ui/wrappers.js +14 -0
- package/dist/ui/wrappers.js.map +19 -0
- package/dist/ui.js +17 -0
- package/dist/ui.js.map +20 -0
- package/package.json +67 -15
- package/src/index.ts +67 -1
- package/src/runtime.ts +4 -0
- package/src/theme.ts +4 -0
- package/src/ui/animation.ts +2 -0
- package/src/ui/ansi.ts +2 -0
- package/src/ui/cli.ts +2 -0
- package/src/ui/display.ts +2 -0
- package/src/ui/image.ts +2 -0
- package/src/ui/input.ts +2 -0
- package/src/ui/progress.ts +2 -0
- package/src/ui/react.ts +2 -0
- package/src/ui/utils.ts +2 -0
- package/src/ui/wrappers.ts +2 -0
- package/src/ui.ts +4 -0
- package/examples/CLAUDE.md +0 -75
- package/examples/_banner.tsx +0 -60
- package/examples/cli.ts +0 -228
- package/examples/index.md +0 -101
- package/examples/inline/inline-nontty.tsx +0 -98
- package/examples/inline/inline-progress.tsx +0 -79
- package/examples/inline/inline-simple.tsx +0 -63
- package/examples/inline/scrollback.tsx +0 -185
- package/examples/interactive/_input-debug.tsx +0 -110
- package/examples/interactive/_stdin-test.ts +0 -71
- package/examples/interactive/_textarea-bare.tsx +0 -45
- package/examples/interactive/aichat/components.tsx +0 -468
- package/examples/interactive/aichat/index.tsx +0 -207
- package/examples/interactive/aichat/script.ts +0 -460
- package/examples/interactive/aichat/state.ts +0 -326
- package/examples/interactive/aichat/types.ts +0 -19
- package/examples/interactive/app-todo.tsx +0 -198
- package/examples/interactive/async-data.tsx +0 -208
- package/examples/interactive/cli-wizard.tsx +0 -332
- package/examples/interactive/clipboard.tsx +0 -183
- package/examples/interactive/components.tsx +0 -463
- package/examples/interactive/data-explorer.tsx +0 -506
- package/examples/interactive/dev-tools.tsx +0 -379
- package/examples/interactive/explorer.tsx +0 -747
- package/examples/interactive/gallery.tsx +0 -652
- package/examples/interactive/inline-bench.tsx +0 -136
- package/examples/interactive/kanban.tsx +0 -267
- package/examples/interactive/layout-ref.tsx +0 -185
- package/examples/interactive/outline.tsx +0 -171
- package/examples/interactive/paste-demo.tsx +0 -198
- package/examples/interactive/scroll.tsx +0 -77
- package/examples/interactive/search-filter.tsx +0 -240
- package/examples/interactive/task-list.tsx +0 -279
- package/examples/interactive/terminal.tsx +0 -798
- package/examples/interactive/textarea.tsx +0 -103
- package/examples/interactive/theme.tsx +0 -336
- package/examples/interactive/transform.tsx +0 -256
- package/examples/interactive/virtual-10k.tsx +0 -413
- package/examples/kitty/canvas.tsx +0 -519
- package/examples/kitty/generate-samples.ts +0 -236
- package/examples/kitty/image-component.tsx +0 -273
- package/examples/kitty/images.tsx +0 -604
- package/examples/kitty/input.tsx +0 -371
- package/examples/kitty/keys.tsx +0 -378
- package/examples/kitty/paint.tsx +0 -1017
- package/examples/layout/dashboard.tsx +0 -551
- package/examples/layout/live-resize.tsx +0 -290
- package/examples/layout/overflow.tsx +0 -51
- package/examples/playground/README.md +0 -69
- package/examples/playground/build.ts +0 -61
- package/examples/playground/index.html +0 -420
- package/examples/playground/playground-app.tsx +0 -416
- package/examples/runtime/elm-counter.tsx +0 -206
- package/examples/runtime/hello-runtime.tsx +0 -73
- package/examples/runtime/pipe-composition.tsx +0 -184
- package/examples/runtime/run-counter.tsx +0 -78
- package/examples/runtime/runtime-counter.tsx +0 -197
- package/examples/screenshots/generate.tsx +0 -563
- package/examples/scrollback-perf.tsx +0 -230
- package/examples/viewer.tsx +0 -654
- package/examples/web/build.ts +0 -365
- package/examples/web/canvas-app.tsx +0 -80
- package/examples/web/canvas.html +0 -89
- package/examples/web/dom-app.tsx +0 -81
- package/examples/web/dom.html +0 -113
- package/examples/web/showcase-app.tsx +0 -107
- package/examples/web/showcase.html +0 -34
- package/examples/web/showcases/index.tsx +0 -56
- package/examples/web/viewer-app.tsx +0 -555
- package/examples/web/viewer.html +0 -30
- package/examples/web/xterm-app.tsx +0 -105
- package/examples/web/xterm.html +0 -118
package/examples/viewer.tsx
DELETED
|
@@ -1,654 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* silvery Examples Viewer
|
|
3
|
-
*
|
|
4
|
-
* Storybook-style TUI for browsing and running silvery examples.
|
|
5
|
-
* Left: nav sidebar. Right: tabbed content (View / Source).
|
|
6
|
-
*
|
|
7
|
-
* Examples are auto-discovered from category directories (layout/, interactive/,
|
|
8
|
-
* runtime/, inline/). Each example exports a `meta` object with name and description.
|
|
9
|
-
* Category is inferred from the directory name.
|
|
10
|
-
*
|
|
11
|
-
* Usage: bun examples (or: bun examples/viewer.tsx)
|
|
12
|
-
*
|
|
13
|
-
* Controls:
|
|
14
|
-
* j/k or arrows - Navigate examples
|
|
15
|
-
* Ctrl+K - Command palette (switch examples)
|
|
16
|
-
* s - Settings (theme picker)
|
|
17
|
-
* Tab - Toggle View / Source tab
|
|
18
|
-
* Enter - Run selected example standalone
|
|
19
|
-
* q/Escape - Quit
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import React, { useState, useCallback, useMemo, useEffect } from "react"
|
|
23
|
-
import { readFileSync } from "node:fs"
|
|
24
|
-
import { resolve } from "node:path"
|
|
25
|
-
import {
|
|
26
|
-
render,
|
|
27
|
-
renderStatic,
|
|
28
|
-
Box,
|
|
29
|
-
Text,
|
|
30
|
-
Spacer,
|
|
31
|
-
ThemeProvider,
|
|
32
|
-
builtinThemes,
|
|
33
|
-
useInput,
|
|
34
|
-
useApp,
|
|
35
|
-
useContentRect,
|
|
36
|
-
createTerm,
|
|
37
|
-
PickerDialog,
|
|
38
|
-
type Key,
|
|
39
|
-
type Theme,
|
|
40
|
-
} from "../src/index.js"
|
|
41
|
-
|
|
42
|
-
// Ctrl+K is the universal command palette shortcut in terminals
|
|
43
|
-
// (Cmd+K requires Kitty protocol which isn't always available)
|
|
44
|
-
const MOD_KEY = "Ctrl"
|
|
45
|
-
|
|
46
|
-
// =============================================================================
|
|
47
|
-
// Auto-Discovery
|
|
48
|
-
// =============================================================================
|
|
49
|
-
|
|
50
|
-
interface Example {
|
|
51
|
-
name: string
|
|
52
|
-
file: string
|
|
53
|
-
description: string
|
|
54
|
-
category: string
|
|
55
|
-
/** Export name of the main component (enables live preview) */
|
|
56
|
-
component?: string
|
|
57
|
-
/** API features showcased */
|
|
58
|
-
features?: string[]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const CATEGORY_DIRS = ["layout", "interactive", "runtime", "inline", "kitty"] as const
|
|
62
|
-
|
|
63
|
-
const CATEGORY_ORDER: Record<string, number> = {
|
|
64
|
-
Layout: 0,
|
|
65
|
-
Interactive: 1,
|
|
66
|
-
Runtime: 2,
|
|
67
|
-
Inline: 3,
|
|
68
|
-
"Kitty Protocol": 4,
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const CATEGORY_COLOR: Record<string, string> = {
|
|
72
|
-
Layout: "magenta",
|
|
73
|
-
Interactive: "cyan",
|
|
74
|
-
Runtime: "green",
|
|
75
|
-
Inline: "yellow",
|
|
76
|
-
"Kitty Protocol": "blue",
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function discoverExamples(): Promise<Example[]> {
|
|
80
|
-
const baseDir = new URL(".", import.meta.url).pathname
|
|
81
|
-
const results: Example[] = []
|
|
82
|
-
|
|
83
|
-
const CATEGORY_DISPLAY: Record<string, string> = { kitty: "Kitty Protocol" }
|
|
84
|
-
|
|
85
|
-
for (const dir of CATEGORY_DIRS) {
|
|
86
|
-
const category = CATEGORY_DISPLAY[dir] ?? dir.charAt(0).toUpperCase() + dir.slice(1)
|
|
87
|
-
const dirPath = resolve(baseDir, dir)
|
|
88
|
-
const files = [
|
|
89
|
-
...new Bun.Glob("*.tsx").scanSync({ cwd: dirPath }),
|
|
90
|
-
...new Bun.Glob("*/index.tsx").scanSync({ cwd: dirPath }),
|
|
91
|
-
]
|
|
92
|
-
|
|
93
|
-
for (const file of files) {
|
|
94
|
-
try {
|
|
95
|
-
const mod = await import(resolve(dirPath, file))
|
|
96
|
-
if (!mod.meta?.name || !mod.meta?.demo) continue
|
|
97
|
-
|
|
98
|
-
// Find first exported function that isn't meta or default
|
|
99
|
-
let component: string | undefined
|
|
100
|
-
for (const [key, value] of Object.entries(mod)) {
|
|
101
|
-
if (key === "meta" || key === "default") continue
|
|
102
|
-
if (typeof value === "function") {
|
|
103
|
-
component = key
|
|
104
|
-
break
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
results.push({
|
|
109
|
-
name: mod.meta.name,
|
|
110
|
-
description: mod.meta.description ?? "",
|
|
111
|
-
file: `${dir}/${file}`,
|
|
112
|
-
category,
|
|
113
|
-
component,
|
|
114
|
-
features: mod.meta.features,
|
|
115
|
-
})
|
|
116
|
-
} catch {
|
|
117
|
-
// Skip files that fail to import
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
results.sort((a, b) => {
|
|
123
|
-
const catDiff = (CATEGORY_ORDER[a.category] ?? 99) - (CATEGORY_ORDER[b.category] ?? 99)
|
|
124
|
-
if (catDiff !== 0) return catDiff
|
|
125
|
-
return a.name.localeCompare(b.name)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
return results
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// =============================================================================
|
|
132
|
-
// Syntax Highlighting
|
|
133
|
-
// =============================================================================
|
|
134
|
-
|
|
135
|
-
const KEYWORDS = new Set([
|
|
136
|
-
"import",
|
|
137
|
-
"from",
|
|
138
|
-
"export",
|
|
139
|
-
"default",
|
|
140
|
-
"function",
|
|
141
|
-
"const",
|
|
142
|
-
"let",
|
|
143
|
-
"var",
|
|
144
|
-
"return",
|
|
145
|
-
"if",
|
|
146
|
-
"else",
|
|
147
|
-
"for",
|
|
148
|
-
"while",
|
|
149
|
-
"switch",
|
|
150
|
-
"case",
|
|
151
|
-
"break",
|
|
152
|
-
"new",
|
|
153
|
-
"typeof",
|
|
154
|
-
"instanceof",
|
|
155
|
-
"async",
|
|
156
|
-
"await",
|
|
157
|
-
"yield",
|
|
158
|
-
"class",
|
|
159
|
-
"extends",
|
|
160
|
-
"implements",
|
|
161
|
-
"interface",
|
|
162
|
-
"type",
|
|
163
|
-
"enum",
|
|
164
|
-
"true",
|
|
165
|
-
"false",
|
|
166
|
-
"null",
|
|
167
|
-
"undefined",
|
|
168
|
-
"this",
|
|
169
|
-
"super",
|
|
170
|
-
"of",
|
|
171
|
-
"in",
|
|
172
|
-
"as",
|
|
173
|
-
"using",
|
|
174
|
-
])
|
|
175
|
-
|
|
176
|
-
const REACT_KEYWORDS = new Set([
|
|
177
|
-
"useState",
|
|
178
|
-
"useEffect",
|
|
179
|
-
"useCallback",
|
|
180
|
-
"useMemo",
|
|
181
|
-
"useRef",
|
|
182
|
-
"useInput",
|
|
183
|
-
"useApp",
|
|
184
|
-
"useTerm",
|
|
185
|
-
"useContentRect",
|
|
186
|
-
"useScrollback",
|
|
187
|
-
])
|
|
188
|
-
|
|
189
|
-
function highlightLine(line: string): React.ReactNode {
|
|
190
|
-
if (line.trimStart().startsWith("//") || line.trimStart().startsWith("*") || line.trimStart().startsWith("/*")) {
|
|
191
|
-
return (
|
|
192
|
-
<Text dim color="gray">
|
|
193
|
-
{line}
|
|
194
|
-
</Text>
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const parts: React.ReactNode[] = []
|
|
199
|
-
const regex =
|
|
200
|
-
/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(<\/?[A-Z]\w*)|(\b[a-zA-Z_]\w*\b)|(\s+)|([^\s"'`<\w]+)/g
|
|
201
|
-
let match: RegExpExecArray | null
|
|
202
|
-
let i = 0
|
|
203
|
-
|
|
204
|
-
while ((match = regex.exec(line)) !== null) {
|
|
205
|
-
const [full, str, jsxTag, word] = match
|
|
206
|
-
if (str) {
|
|
207
|
-
parts.push(
|
|
208
|
-
<Text key={i++} color="green">
|
|
209
|
-
{str}
|
|
210
|
-
</Text>,
|
|
211
|
-
)
|
|
212
|
-
} else if (jsxTag) {
|
|
213
|
-
parts.push(
|
|
214
|
-
<Text key={i++} color="cyan">
|
|
215
|
-
{jsxTag}
|
|
216
|
-
</Text>,
|
|
217
|
-
)
|
|
218
|
-
} else if (word && KEYWORDS.has(word)) {
|
|
219
|
-
parts.push(
|
|
220
|
-
<Text key={i++} color="magenta" bold>
|
|
221
|
-
{word}
|
|
222
|
-
</Text>,
|
|
223
|
-
)
|
|
224
|
-
} else if (word && REACT_KEYWORDS.has(word)) {
|
|
225
|
-
parts.push(
|
|
226
|
-
<Text key={i++} color="yellow">
|
|
227
|
-
{word}
|
|
228
|
-
</Text>,
|
|
229
|
-
)
|
|
230
|
-
} else {
|
|
231
|
-
parts.push(<Text key={i++}>{full}</Text>)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return parts.length > 0 ? <>{parts}</> : <Text>{line}</Text>
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// =============================================================================
|
|
239
|
-
// Components
|
|
240
|
-
// =============================================================================
|
|
241
|
-
|
|
242
|
-
function Sidebar({ examples, cursor, theme }: { examples: Example[]; cursor: number; theme: Theme }) {
|
|
243
|
-
const { groups, scrollToChild } = useMemo(() => {
|
|
244
|
-
const result: {
|
|
245
|
-
category: string
|
|
246
|
-
items: { example: Example; globalIdx: number }[]
|
|
247
|
-
}[] = []
|
|
248
|
-
let currentCat = ""
|
|
249
|
-
let childIdx = 0
|
|
250
|
-
let targetChild = 0
|
|
251
|
-
|
|
252
|
-
for (let i = 0; i < examples.length; i++) {
|
|
253
|
-
const ex = examples[i]!
|
|
254
|
-
if (ex.category !== currentCat) {
|
|
255
|
-
currentCat = ex.category
|
|
256
|
-
result.push({ category: currentCat, items: [] })
|
|
257
|
-
childIdx++
|
|
258
|
-
}
|
|
259
|
-
if (i === cursor) targetChild = childIdx
|
|
260
|
-
result[result.length - 1]!.items.push({ example: ex, globalIdx: i })
|
|
261
|
-
childIdx++
|
|
262
|
-
}
|
|
263
|
-
return { groups: result, scrollToChild: targetChild }
|
|
264
|
-
}, [examples, cursor])
|
|
265
|
-
|
|
266
|
-
return (
|
|
267
|
-
<Box
|
|
268
|
-
flexDirection="column"
|
|
269
|
-
width={28}
|
|
270
|
-
borderStyle="round"
|
|
271
|
-
borderColor="$border"
|
|
272
|
-
overflow="scroll"
|
|
273
|
-
scrollTo={scrollToChild}
|
|
274
|
-
>
|
|
275
|
-
{groups.map((group) => (
|
|
276
|
-
<React.Fragment key={group.category}>
|
|
277
|
-
<Box paddingX={1}>
|
|
278
|
-
<Text bold color={CATEGORY_COLOR[group.category] ?? "$text"} dim>
|
|
279
|
-
{group.category}
|
|
280
|
-
</Text>
|
|
281
|
-
</Box>
|
|
282
|
-
{group.items.map(({ example, globalIdx }) => {
|
|
283
|
-
const selected = globalIdx === cursor
|
|
284
|
-
return (
|
|
285
|
-
<Box key={example.name} paddingX={1} backgroundColor={selected ? "$primary" : undefined}>
|
|
286
|
-
<Text color={selected ? "$text" : "$text"} bold={selected} wrap="truncate">
|
|
287
|
-
{selected ? "\u25B8 " : " "}
|
|
288
|
-
{example.name}
|
|
289
|
-
</Text>
|
|
290
|
-
</Box>
|
|
291
|
-
)
|
|
292
|
-
})}
|
|
293
|
-
</React.Fragment>
|
|
294
|
-
))}
|
|
295
|
-
</Box>
|
|
296
|
-
)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** Pad content lines to fill the full height — prevents stale pixel artifacts
|
|
300
|
-
* from the incremental renderer when switching between previews of different heights. */
|
|
301
|
-
function padLines(contentLines: string[], totalHeight: number): string[] {
|
|
302
|
-
if (contentLines.length >= totalHeight) return contentLines.slice(0, totalHeight)
|
|
303
|
-
return [...contentLines, ...Array<string>(totalHeight - contentLines.length).fill("")]
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function Preview({ example, theme }: { example: Example; theme: Theme }) {
|
|
307
|
-
const { width, height } = useContentRect()
|
|
308
|
-
const [lines, setLines] = useState<string[] | null>(null)
|
|
309
|
-
const [error, setError] = useState<string | null>(null)
|
|
310
|
-
|
|
311
|
-
useEffect(() => {
|
|
312
|
-
setLines(null)
|
|
313
|
-
setError(null)
|
|
314
|
-
|
|
315
|
-
if (!example.component) {
|
|
316
|
-
setError("no-component")
|
|
317
|
-
return
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Wait for layout dimensions
|
|
321
|
-
if (width === 0 || height === 0) return
|
|
322
|
-
|
|
323
|
-
let cancelled = false
|
|
324
|
-
const path = new URL(example.file, import.meta.url).pathname
|
|
325
|
-
|
|
326
|
-
import(path)
|
|
327
|
-
.then(async (mod: Record<string, unknown>) => {
|
|
328
|
-
if (cancelled) return
|
|
329
|
-
const Comp = mod[example.component!] as React.ComponentType | undefined
|
|
330
|
-
if (!Comp) {
|
|
331
|
-
setError(`Export "${example.component}" not found`)
|
|
332
|
-
return
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Render in sandboxed static mode — useInput becomes a no-op,
|
|
336
|
-
// useApp gets a stub exit(), no terminal needed.
|
|
337
|
-
// Wrap in ThemeProvider so previews pick up the active theme.
|
|
338
|
-
const output = await renderStatic(React.createElement(ThemeProvider, { theme }, React.createElement(Comp)), {
|
|
339
|
-
width,
|
|
340
|
-
height,
|
|
341
|
-
})
|
|
342
|
-
if (!cancelled) setLines(output.split("\n"))
|
|
343
|
-
return undefined
|
|
344
|
-
})
|
|
345
|
-
.catch((e: Error) => {
|
|
346
|
-
if (!cancelled) setError(e.message || String(e))
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
return () => {
|
|
350
|
-
cancelled = true
|
|
351
|
-
}
|
|
352
|
-
}, [example.file, example.component, width, height])
|
|
353
|
-
|
|
354
|
-
// All paths pad to full height to clear stale pixels from prior previews
|
|
355
|
-
const renderLines = (contentLines: string[]) => (
|
|
356
|
-
<Box flexDirection="column" flexGrow={1}>
|
|
357
|
-
{padLines(contentLines, height).map((line, i) => (
|
|
358
|
-
<Text key={i} wrap="truncate">
|
|
359
|
-
{line}
|
|
360
|
-
</Text>
|
|
361
|
-
))}
|
|
362
|
-
</Box>
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
if (error === "no-component") {
|
|
366
|
-
return renderLines(["", " No live preview — uses non-React API.", " Press Enter to run standalone."])
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (error) {
|
|
370
|
-
return renderLines(["", ` Error: ${error}`])
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (!lines) {
|
|
374
|
-
return renderLines(["", " Loading preview..."])
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return renderLines(lines)
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function SourceCode({ example }: { example: Example }) {
|
|
381
|
-
const lines = useMemo(() => {
|
|
382
|
-
try {
|
|
383
|
-
const path = new URL(example.file, import.meta.url).pathname
|
|
384
|
-
return readFileSync(path, "utf-8").split("\n")
|
|
385
|
-
} catch {
|
|
386
|
-
return ["// Could not load file"]
|
|
387
|
-
}
|
|
388
|
-
}, [example.file])
|
|
389
|
-
|
|
390
|
-
return (
|
|
391
|
-
<Box flexDirection="column" flexGrow={1} paddingX={1}>
|
|
392
|
-
{lines.map((line, i) => (
|
|
393
|
-
<Text key={i} wrap="truncate">
|
|
394
|
-
<Text dim color="gray">
|
|
395
|
-
{String(i + 1).padStart(3)}{" "}
|
|
396
|
-
</Text>
|
|
397
|
-
{highlightLine(line)}
|
|
398
|
-
</Text>
|
|
399
|
-
))}
|
|
400
|
-
</Box>
|
|
401
|
-
)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const THEME_NAMES = Object.keys(builtinThemes)
|
|
405
|
-
|
|
406
|
-
type Dialog = "none" | "command-palette" | "settings"
|
|
407
|
-
|
|
408
|
-
function Viewer({ examples }: { examples: Example[] }) {
|
|
409
|
-
const { exit } = useApp()
|
|
410
|
-
const [cursor, setCursor] = useState(0)
|
|
411
|
-
const [tab, setTab] = useState<"view" | "source">("view")
|
|
412
|
-
const [running, setRunning] = useState<string | null>(null)
|
|
413
|
-
const [themeIdx, setThemeIdx] = useState(THEME_NAMES.indexOf("ansi16-dark"))
|
|
414
|
-
const [dialog, setDialog] = useState<Dialog>("none")
|
|
415
|
-
const [paletteQuery, setPaletteQuery] = useState("")
|
|
416
|
-
const [themeQuery, setThemeQuery] = useState("")
|
|
417
|
-
|
|
418
|
-
const theme = builtinThemes[THEME_NAMES[themeIdx]!]!
|
|
419
|
-
const maxCursor = examples.length - 1
|
|
420
|
-
const selected = examples[cursor]!
|
|
421
|
-
|
|
422
|
-
const runExample = useCallback(
|
|
423
|
-
(idx: number) => {
|
|
424
|
-
const example = examples[idx]
|
|
425
|
-
if (!example) return
|
|
426
|
-
setRunning(example.name)
|
|
427
|
-
exit()
|
|
428
|
-
|
|
429
|
-
const file = new URL(example.file, import.meta.url).pathname
|
|
430
|
-
const proc = Bun.spawn(["bun", "run", file], {
|
|
431
|
-
stdio: ["inherit", "inherit", "inherit"],
|
|
432
|
-
env: { ...process.env, SILVERY_THEME: theme.name },
|
|
433
|
-
})
|
|
434
|
-
void proc.exited.then(() => process.exit(0))
|
|
435
|
-
},
|
|
436
|
-
[examples, exit, theme.name],
|
|
437
|
-
)
|
|
438
|
-
|
|
439
|
-
// --- Command palette items ---
|
|
440
|
-
const paletteItems = useMemo(() => {
|
|
441
|
-
const q = paletteQuery.toLowerCase()
|
|
442
|
-
return examples
|
|
443
|
-
.map((ex, idx) => ({ ...ex, idx }))
|
|
444
|
-
.filter((ex) => !q || ex.name.toLowerCase().includes(q) || ex.category.toLowerCase().includes(q))
|
|
445
|
-
}, [examples, paletteQuery])
|
|
446
|
-
|
|
447
|
-
// --- Theme picker items ---
|
|
448
|
-
const themeItems = useMemo(() => {
|
|
449
|
-
const q = themeQuery.toLowerCase()
|
|
450
|
-
return THEME_NAMES.filter((name) => !q || name.toLowerCase().includes(q))
|
|
451
|
-
}, [themeQuery])
|
|
452
|
-
|
|
453
|
-
useInput((input: string, key: Key) => {
|
|
454
|
-
if (running || dialog !== "none") return
|
|
455
|
-
|
|
456
|
-
if (input === "q" || key.escape) {
|
|
457
|
-
exit()
|
|
458
|
-
return
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Ctrl+K — command palette
|
|
462
|
-
if (input === "k" && key.ctrl) {
|
|
463
|
-
setPaletteQuery("")
|
|
464
|
-
setDialog("command-palette")
|
|
465
|
-
return
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (key.tab) {
|
|
469
|
-
setTab((t) => (t === "view" ? "source" : "view"))
|
|
470
|
-
return
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (key.downArrow || input === "j") {
|
|
474
|
-
setCursor((prev) => Math.min(maxCursor, prev + 1))
|
|
475
|
-
}
|
|
476
|
-
if (key.upArrow || input === "k") {
|
|
477
|
-
setCursor((prev) => Math.max(0, prev - 1))
|
|
478
|
-
}
|
|
479
|
-
if (key.home || input === "g") {
|
|
480
|
-
setCursor(0)
|
|
481
|
-
}
|
|
482
|
-
if (key.end || input === "G") {
|
|
483
|
-
setCursor(maxCursor)
|
|
484
|
-
}
|
|
485
|
-
if (key.return) {
|
|
486
|
-
runExample(cursor)
|
|
487
|
-
}
|
|
488
|
-
if (input === "s") {
|
|
489
|
-
setThemeQuery("")
|
|
490
|
-
setDialog("settings")
|
|
491
|
-
}
|
|
492
|
-
})
|
|
493
|
-
|
|
494
|
-
if (running) {
|
|
495
|
-
return (
|
|
496
|
-
<ThemeProvider theme={theme}>
|
|
497
|
-
<Box padding={1}>
|
|
498
|
-
<Text color="$muted">Launching {running}...</Text>
|
|
499
|
-
</Box>
|
|
500
|
-
</ThemeProvider>
|
|
501
|
-
)
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Derive URL key from file path (e.g., "interactive/kanban.tsx" → "kanban")
|
|
505
|
-
const exampleKey = selected.file.replace(/^.*\//, "").replace(/\.tsx$/, "")
|
|
506
|
-
|
|
507
|
-
return (
|
|
508
|
-
<ThemeProvider theme={theme}>
|
|
509
|
-
<Box flexDirection="column" flexGrow={1}>
|
|
510
|
-
{/* Header */}
|
|
511
|
-
<Box paddingX={1}>
|
|
512
|
-
<Text bold color="$warning">
|
|
513
|
-
{" silvery"}
|
|
514
|
-
</Text>
|
|
515
|
-
<Text color="$muted"> examples </Text>
|
|
516
|
-
<Text color="$muted">
|
|
517
|
-
({cursor + 1}/{examples.length})
|
|
518
|
-
</Text>
|
|
519
|
-
<Spacer />
|
|
520
|
-
<Text color="$muted">
|
|
521
|
-
theme:{" "}
|
|
522
|
-
<Text color="$primary" bold>
|
|
523
|
-
{theme.name}
|
|
524
|
-
</Text>
|
|
525
|
-
</Text>
|
|
526
|
-
</Box>
|
|
527
|
-
|
|
528
|
-
{/* Main: sidebar + content */}
|
|
529
|
-
<Box flexDirection="row" flexGrow={1} gap={1}>
|
|
530
|
-
<Sidebar examples={examples} cursor={cursor} theme={theme} />
|
|
531
|
-
|
|
532
|
-
{/* Content area with tabs */}
|
|
533
|
-
<Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="$border" overflow="hidden">
|
|
534
|
-
{/* Info banner */}
|
|
535
|
-
<Box paddingX={1} flexDirection="column">
|
|
536
|
-
<Text wrap="truncate">
|
|
537
|
-
<Text bold color="$text">
|
|
538
|
-
{selected.name}
|
|
539
|
-
</Text>
|
|
540
|
-
<Text color="$muted"> — {selected.description}</Text>
|
|
541
|
-
</Text>
|
|
542
|
-
{selected.features && selected.features.length > 0 && (
|
|
543
|
-
<Text color="$muted" wrap="truncate">
|
|
544
|
-
{selected.features.join(" · ")}
|
|
545
|
-
</Text>
|
|
546
|
-
)}
|
|
547
|
-
<Text color="$muted" dim wrap="truncate">
|
|
548
|
-
silvery.dev/examples/{exampleKey}
|
|
549
|
-
</Text>
|
|
550
|
-
</Box>
|
|
551
|
-
|
|
552
|
-
{/* Tab bar */}
|
|
553
|
-
<Box paddingX={1}>
|
|
554
|
-
<Text>
|
|
555
|
-
<Text bold={tab === "view"} color={tab === "view" ? "$primary" : "$muted"}>
|
|
556
|
-
View
|
|
557
|
-
</Text>
|
|
558
|
-
<Text color="$border"> | </Text>
|
|
559
|
-
<Text bold={tab === "source"} color={tab === "source" ? "$primary" : "$muted"}>
|
|
560
|
-
Source
|
|
561
|
-
</Text>
|
|
562
|
-
</Text>
|
|
563
|
-
</Box>
|
|
564
|
-
|
|
565
|
-
{/* Tab content — key forces full teardown on example switch */}
|
|
566
|
-
{tab === "view" ? (
|
|
567
|
-
<Box key={selected.file} flexDirection="column" flexGrow={1} overflow="hidden">
|
|
568
|
-
<Preview example={selected} theme={theme} />
|
|
569
|
-
</Box>
|
|
570
|
-
) : (
|
|
571
|
-
<SourceCode key={selected.file} example={selected} />
|
|
572
|
-
)}
|
|
573
|
-
</Box>
|
|
574
|
-
</Box>
|
|
575
|
-
|
|
576
|
-
{/* Bottom bar */}
|
|
577
|
-
<Box paddingX={1}>
|
|
578
|
-
<Text color="$muted">
|
|
579
|
-
<Text bold>{MOD_KEY}-K</Text> switch <Text bold>s</Text> settings <Text bold>Tab</Text>{" "}
|
|
580
|
-
{tab === "view" ? "source" : "view"} <Text bold>Enter</Text> run <Text bold>q</Text> quit
|
|
581
|
-
</Text>
|
|
582
|
-
</Box>
|
|
583
|
-
|
|
584
|
-
{/* Command palette (Cmd-K) */}
|
|
585
|
-
{dialog === "command-palette" && (
|
|
586
|
-
<PickerDialog
|
|
587
|
-
title="Switch Example"
|
|
588
|
-
placeholder="Type to search..."
|
|
589
|
-
items={paletteItems}
|
|
590
|
-
renderItem={(item, sel) => (
|
|
591
|
-
<Text color={sel ? "$primary" : "$text"} bold={sel}>
|
|
592
|
-
<Text color="$muted" dim>
|
|
593
|
-
{item.category}
|
|
594
|
-
{" / "}
|
|
595
|
-
</Text>
|
|
596
|
-
{item.name}
|
|
597
|
-
</Text>
|
|
598
|
-
)}
|
|
599
|
-
keyExtractor={(item) => item.file}
|
|
600
|
-
onSelect={(item) => {
|
|
601
|
-
setCursor(item.idx)
|
|
602
|
-
setDialog("none")
|
|
603
|
-
}}
|
|
604
|
-
onCancel={() => setDialog("none")}
|
|
605
|
-
onChange={setPaletteQuery}
|
|
606
|
-
/>
|
|
607
|
-
)}
|
|
608
|
-
|
|
609
|
-
{/* Settings / theme picker (s key) */}
|
|
610
|
-
{dialog === "settings" && (
|
|
611
|
-
<PickerDialog
|
|
612
|
-
title="Theme"
|
|
613
|
-
placeholder="Type to filter themes..."
|
|
614
|
-
items={themeItems}
|
|
615
|
-
renderItem={(name, sel) => {
|
|
616
|
-
const t = builtinThemes[name]!
|
|
617
|
-
return (
|
|
618
|
-
<Text color={sel ? "$primary" : "$text"} bold={sel}>
|
|
619
|
-
{name === THEME_NAMES[themeIdx] ? "* " : " "}
|
|
620
|
-
{name}
|
|
621
|
-
<Text color="$muted" dim>
|
|
622
|
-
{" "}
|
|
623
|
-
{t.dark ? "dark" : "light"}
|
|
624
|
-
</Text>
|
|
625
|
-
</Text>
|
|
626
|
-
)
|
|
627
|
-
}}
|
|
628
|
-
keyExtractor={(name) => name}
|
|
629
|
-
onSelect={(name) => {
|
|
630
|
-
setThemeIdx(THEME_NAMES.indexOf(name))
|
|
631
|
-
setDialog("none")
|
|
632
|
-
}}
|
|
633
|
-
onCancel={() => setDialog("none")}
|
|
634
|
-
onChange={setThemeQuery}
|
|
635
|
-
/>
|
|
636
|
-
)}
|
|
637
|
-
</Box>
|
|
638
|
-
</ThemeProvider>
|
|
639
|
-
)
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// =============================================================================
|
|
643
|
-
// Main
|
|
644
|
-
// =============================================================================
|
|
645
|
-
|
|
646
|
-
async function main() {
|
|
647
|
-
const examples = await discoverExamples()
|
|
648
|
-
|
|
649
|
-
using term = createTerm()
|
|
650
|
-
const { waitUntilExit } = await render(<Viewer examples={examples} />, term)
|
|
651
|
-
await waitUntilExit()
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
main().catch(console.error)
|