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
|
@@ -1,555 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified Web Viewer for Silvery Examples
|
|
3
|
-
*
|
|
4
|
-
* A DOM app (vanilla TypeScript, no React) that builds chrome around an xterm.js
|
|
5
|
-
* terminal pane. Showcases run via renderToXterm() inside the terminal.
|
|
6
|
-
*
|
|
7
|
-
* Layout:
|
|
8
|
-
* Sidebar (220px) | Demo pane (flex) | Source pane (380px, collapsible)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { Terminal } from "@xterm/xterm"
|
|
12
|
-
import { FitAddon } from "@xterm/addon-fit"
|
|
13
|
-
import { renderToXterm } from "../../packages/term/src/xterm/index.js"
|
|
14
|
-
import { SHOWCASES } from "./showcases/index.js"
|
|
15
|
-
import { REGISTRY, type ExampleEntry } from "./viewer-registry.js"
|
|
16
|
-
import React from "react"
|
|
17
|
-
|
|
18
|
-
// =============================================================================
|
|
19
|
-
// Types
|
|
20
|
-
// =============================================================================
|
|
21
|
-
|
|
22
|
-
interface DemoEntry {
|
|
23
|
-
id: string
|
|
24
|
-
name: string
|
|
25
|
-
description: string
|
|
26
|
-
category: string
|
|
27
|
-
features: string[]
|
|
28
|
-
source: string
|
|
29
|
-
component?: () => JSX.Element
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface Category {
|
|
33
|
-
name: string
|
|
34
|
-
color: string
|
|
35
|
-
items: DemoEntry[]
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const CATEGORY_CONFIG: Record<string, { color: string; order: number }> = {
|
|
39
|
-
Showcases: { color: "#f9e2af", order: 0 },
|
|
40
|
-
Layout: { color: "#cba6f7", order: 1 },
|
|
41
|
-
Interactive: { color: "#89dceb", order: 2 },
|
|
42
|
-
Runtime: { color: "#a6e3a1", order: 3 },
|
|
43
|
-
Inline: { color: "#fab387", order: 4 },
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function buildRegistry(): { categories: Category[]; allDemos: DemoEntry[] } {
|
|
47
|
-
const allDemos: DemoEntry[] = []
|
|
48
|
-
|
|
49
|
-
// Build a lookup from the auto-generated registry (keyed by showcase-<id>)
|
|
50
|
-
const registryByKey = new Map<string, ExampleEntry>()
|
|
51
|
-
for (const entry of REGISTRY) {
|
|
52
|
-
registryByKey.set(entry.key, entry)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Showcases: SHOWCASES registry provides components, REGISTRY provides metadata
|
|
56
|
-
for (const [id, component] of Object.entries(SHOWCASES)) {
|
|
57
|
-
const entry = registryByKey.get(`showcase-${id}`)
|
|
58
|
-
allDemos.push({
|
|
59
|
-
id,
|
|
60
|
-
name: entry?.name ?? id,
|
|
61
|
-
description: entry?.description ?? "",
|
|
62
|
-
category: "Showcases",
|
|
63
|
-
features: entry?.features ?? [],
|
|
64
|
-
source: entry?.source ?? "",
|
|
65
|
-
component: component as () => JSX.Element,
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Non-showcase examples from the registry
|
|
70
|
-
for (const entry of REGISTRY) {
|
|
71
|
-
if (entry.type === "showcase") continue
|
|
72
|
-
allDemos.push({
|
|
73
|
-
id: entry.key,
|
|
74
|
-
name: entry.name,
|
|
75
|
-
description: entry.description,
|
|
76
|
-
category: entry.category,
|
|
77
|
-
features: entry.features,
|
|
78
|
-
source: entry.source,
|
|
79
|
-
})
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Group by category
|
|
83
|
-
const catMap = new Map<string, DemoEntry[]>()
|
|
84
|
-
for (const demo of allDemos) {
|
|
85
|
-
const list = catMap.get(demo.category) ?? []
|
|
86
|
-
list.push(demo)
|
|
87
|
-
catMap.set(demo.category, list)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const categories: Category[] = []
|
|
91
|
-
for (const [name, items] of catMap) {
|
|
92
|
-
categories.push({
|
|
93
|
-
name,
|
|
94
|
-
color: CATEGORY_CONFIG[name]?.color ?? "#cdd6f4",
|
|
95
|
-
items,
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
categories.sort((a, b) => (CATEGORY_CONFIG[a.name]?.order ?? 99) - (CATEGORY_CONFIG[b.name]?.order ?? 99))
|
|
99
|
-
|
|
100
|
-
return { categories, allDemos }
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// =============================================================================
|
|
104
|
-
// Syntax Highlighting (CSS class-based)
|
|
105
|
-
// =============================================================================
|
|
106
|
-
|
|
107
|
-
const KW = new Set([
|
|
108
|
-
"import",
|
|
109
|
-
"export",
|
|
110
|
-
"from",
|
|
111
|
-
"function",
|
|
112
|
-
"const",
|
|
113
|
-
"let",
|
|
114
|
-
"var",
|
|
115
|
-
"return",
|
|
116
|
-
"if",
|
|
117
|
-
"else",
|
|
118
|
-
"for",
|
|
119
|
-
"while",
|
|
120
|
-
"switch",
|
|
121
|
-
"case",
|
|
122
|
-
"break",
|
|
123
|
-
"new",
|
|
124
|
-
"typeof",
|
|
125
|
-
"instanceof",
|
|
126
|
-
"async",
|
|
127
|
-
"await",
|
|
128
|
-
"yield",
|
|
129
|
-
"class",
|
|
130
|
-
"extends",
|
|
131
|
-
"interface",
|
|
132
|
-
"type",
|
|
133
|
-
"enum",
|
|
134
|
-
"true",
|
|
135
|
-
"false",
|
|
136
|
-
"null",
|
|
137
|
-
"undefined",
|
|
138
|
-
"as",
|
|
139
|
-
"of",
|
|
140
|
-
"in",
|
|
141
|
-
"default",
|
|
142
|
-
"using",
|
|
143
|
-
])
|
|
144
|
-
|
|
145
|
-
function highlightSource(code: string): string {
|
|
146
|
-
return code
|
|
147
|
-
.split("\n")
|
|
148
|
-
.map((line, i) => {
|
|
149
|
-
const num = `<span class="line-num">${String(i + 1).padStart(3)}</span> `
|
|
150
|
-
const trimmed = line.trimStart()
|
|
151
|
-
|
|
152
|
-
// Comment lines
|
|
153
|
-
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
154
|
-
return num + `<span class="hl-comment">${esc(line)}</span>`
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Token-by-token highlighting
|
|
158
|
-
let result = ""
|
|
159
|
-
const re =
|
|
160
|
-
/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(<\/?[A-Z]\w*)|(\b[a-zA-Z_]\w*\b)|(:\s*)([A-Z]\w*)|([^\s"'`<\w]+|\s+)/g
|
|
161
|
-
let m: RegExpExecArray | null
|
|
162
|
-
while ((m = re.exec(line)) !== null) {
|
|
163
|
-
const [full, str, jsxTag, word, colonSpace, typeName] = m
|
|
164
|
-
if (str) {
|
|
165
|
-
result += `<span class="hl-string">${esc(str)}</span>`
|
|
166
|
-
} else if (jsxTag) {
|
|
167
|
-
result += `<span class="hl-jsx">${esc(jsxTag)}</span>`
|
|
168
|
-
} else if (word && KW.has(word)) {
|
|
169
|
-
result += `<span class="hl-keyword">${esc(word)}</span>`
|
|
170
|
-
} else if (colonSpace && typeName) {
|
|
171
|
-
result += `<span class="hl-punct">${esc(colonSpace)}</span><span class="hl-type">${esc(typeName)}</span>`
|
|
172
|
-
} else {
|
|
173
|
-
result += esc(full!)
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return num + result
|
|
178
|
-
})
|
|
179
|
-
.join("\n")
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function esc(s: string): string {
|
|
183
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// =============================================================================
|
|
187
|
-
// DOM Builder
|
|
188
|
-
// =============================================================================
|
|
189
|
-
|
|
190
|
-
function el<K extends keyof HTMLElementTagNameMap>(
|
|
191
|
-
tag: K,
|
|
192
|
-
attrs?: Record<string, string>,
|
|
193
|
-
...children: (HTMLElement | string)[]
|
|
194
|
-
): HTMLElementTagNameMap[K] {
|
|
195
|
-
const e = document.createElement(tag)
|
|
196
|
-
if (attrs) {
|
|
197
|
-
for (const [k, v] of Object.entries(attrs)) {
|
|
198
|
-
if (k === "className") e.className = v
|
|
199
|
-
else e.setAttribute(k, v)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
for (const child of children) {
|
|
203
|
-
if (typeof child === "string") e.appendChild(document.createTextNode(child))
|
|
204
|
-
else e.appendChild(child)
|
|
205
|
-
}
|
|
206
|
-
return e
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// =============================================================================
|
|
210
|
-
// Main App
|
|
211
|
-
// =============================================================================
|
|
212
|
-
|
|
213
|
-
function createViewerApp(root: HTMLElement): void {
|
|
214
|
-
const { categories, allDemos } = buildRegistry()
|
|
215
|
-
if (allDemos.length === 0) {
|
|
216
|
-
root.textContent = "No demos found."
|
|
217
|
-
return
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
let selectedIdx = 0
|
|
221
|
-
let sourceVisible = window.innerWidth >= 900
|
|
222
|
-
let sidebarFocused = false
|
|
223
|
-
let currentInstance: ReturnType<typeof renderToXterm> | null = null
|
|
224
|
-
|
|
225
|
-
// ─── Inject styles ─────────────────────────────────────────────────
|
|
226
|
-
const style = document.createElement("style")
|
|
227
|
-
style.textContent = `
|
|
228
|
-
/* Layout */
|
|
229
|
-
#viewer-root { display: flex; flex-direction: column; width: 100%; height: 100%; }
|
|
230
|
-
.vw-header { display: flex; align-items: center; padding: 8px 16px; background: #13132a; border-bottom: 1px solid #313244; min-height: 44px; gap: 12px; }
|
|
231
|
-
.vw-header-brand { font-size: 18px; font-weight: 700; background: linear-gradient(135deg, #cba6f7, #89dceb); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; letter-spacing: -0.5px; }
|
|
232
|
-
.vw-header-title { font-size: 14px; color: #a6adc8; flex-grow: 1; }
|
|
233
|
-
.vw-header-badge { font-size: 12px; color: #89dceb; background: rgba(137, 220, 235, 0.1); padding: 2px 10px; border-radius: 10px; }
|
|
234
|
-
.vw-body { display: flex; flex: 1; overflow: hidden; }
|
|
235
|
-
|
|
236
|
-
/* Sidebar */
|
|
237
|
-
.vw-sidebar { width: 220px; min-width: 220px; background: #13132a; border-right: 1px solid #313244; overflow-y: auto; padding: 8px 0; }
|
|
238
|
-
.vw-sidebar::-webkit-scrollbar { width: 4px; }
|
|
239
|
-
.vw-sidebar::-webkit-scrollbar-thumb { background: #45475a; border-radius: 2px; }
|
|
240
|
-
.vw-cat-header { padding: 10px 16px 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; display: flex; align-items: center; gap: 8px; }
|
|
241
|
-
.vw-cat-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
|
|
242
|
-
.vw-item { padding: 6px 16px 6px 28px; cursor: pointer; font-size: 13px; color: #a6adc8; transition: background 0.1s, color 0.1s; border-left: 2px solid transparent; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
243
|
-
.vw-item:hover { background: rgba(137, 220, 235, 0.05); color: #cdd6f4; }
|
|
244
|
-
.vw-item.selected { background: rgba(137, 220, 235, 0.1); color: #89dceb; border-left-color: #89dceb; font-weight: 500; }
|
|
245
|
-
|
|
246
|
-
/* Main pane */
|
|
247
|
-
.vw-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
|
248
|
-
.vw-term-wrap { flex: 1; position: relative; overflow: hidden; padding: 4px; }
|
|
249
|
-
.vw-term-wrap .xterm { height: 100% !important; }
|
|
250
|
-
.vw-info { padding: 8px 16px 6px; border-top: 1px solid #313244; }
|
|
251
|
-
.vw-info-name { font-size: 14px; font-weight: 600; color: #cdd6f4; margin-bottom: 2px; display: inline; }
|
|
252
|
-
.vw-info-desc { font-size: 12px; color: #6c7086; margin-bottom: 4px; display: inline; margin-left: 8px; }
|
|
253
|
-
.vw-badges { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
254
|
-
.vw-badge { font-size: 11px; padding: 1px 8px; border-radius: 8px; background: rgba(137, 180, 250, 0.12); color: #89b4fa; }
|
|
255
|
-
.vw-keyhints { font-size: 11px; color: #6c7086; margin-top: 6px; }
|
|
256
|
-
.vw-keyhints kbd { background: #313244; padding: 1px 5px; border-radius: 3px; font-family: inherit; font-size: 10px; margin: 0 2px; }
|
|
257
|
-
|
|
258
|
-
/* Source pane */
|
|
259
|
-
.vw-source { width: 380px; min-width: 380px; background: #11111b; border-left: 1px solid #313244; display: flex; flex-direction: column; overflow: hidden; transition: width 0.2s, min-width 0.2s; }
|
|
260
|
-
.vw-source.hidden { width: 0; min-width: 0; border-left: none; }
|
|
261
|
-
.vw-source-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid #313244; }
|
|
262
|
-
.vw-source-title { font-size: 12px; font-weight: 600; color: #a6adc8; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
263
|
-
.vw-source-btn { background: #313244; border: none; color: #a6adc8; font-size: 11px; padding: 3px 10px; border-radius: 4px; cursor: pointer; transition: background 0.15s; }
|
|
264
|
-
.vw-source-btn:hover { background: #45475a; color: #cdd6f4; }
|
|
265
|
-
.vw-source-code { flex: 1; overflow: auto; padding: 8px 0; }
|
|
266
|
-
.vw-source-code::-webkit-scrollbar { width: 4px; }
|
|
267
|
-
.vw-source-code::-webkit-scrollbar-thumb { background: #45475a; border-radius: 2px; }
|
|
268
|
-
.vw-source-code pre { margin: 0; padding: 0 12px; font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Menlo, monospace; font-size: 12px; line-height: 1.6; color: #cdd6f4; white-space: pre; }
|
|
269
|
-
.line-num { color: #45475a; user-select: none; }
|
|
270
|
-
.hl-comment { color: #6c7086; font-style: italic; }
|
|
271
|
-
.hl-string { color: #a6e3a1; }
|
|
272
|
-
.hl-keyword { color: #cba6f7; }
|
|
273
|
-
.hl-jsx { color: #f9e2af; }
|
|
274
|
-
.hl-type { color: #89dceb; }
|
|
275
|
-
.hl-punct { color: #6c7086; }
|
|
276
|
-
|
|
277
|
-
/* Source toggle (small screens) */
|
|
278
|
-
.vw-source-toggle { position: absolute; top: 8px; right: 8px; background: #313244; border: none; color: #a6adc8; font-size: 12px; padding: 4px 10px; border-radius: 4px; cursor: pointer; z-index: 10; display: none; }
|
|
279
|
-
@media (max-width: 900px) {
|
|
280
|
-
.vw-source-toggle { display: block; }
|
|
281
|
-
.vw-source { position: absolute; right: 0; top: 0; bottom: 0; z-index: 20; box-shadow: -4px 0 20px rgba(0,0,0,0.5); }
|
|
282
|
-
}
|
|
283
|
-
`
|
|
284
|
-
document.head.appendChild(style)
|
|
285
|
-
|
|
286
|
-
// ─── Build DOM ─────────────────────────────────────────────────────
|
|
287
|
-
|
|
288
|
-
// Header
|
|
289
|
-
const header = el(
|
|
290
|
-
"div",
|
|
291
|
-
{ className: "vw-header" },
|
|
292
|
-
el("span", { className: "vw-header-brand" }, "silvery"),
|
|
293
|
-
el("span", { className: "vw-header-title" }, "Interactive Examples"),
|
|
294
|
-
el("span", { className: "vw-header-badge" }, `${allDemos.length} demos`),
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
// Sidebar
|
|
298
|
-
const sidebar = el("div", { className: "vw-sidebar" })
|
|
299
|
-
const sidebarItems: HTMLElement[] = []
|
|
300
|
-
|
|
301
|
-
for (const cat of categories) {
|
|
302
|
-
const catHeader = el("div", { className: "vw-cat-header" })
|
|
303
|
-
const dot = el("span", { className: "vw-cat-dot" })
|
|
304
|
-
dot.style.backgroundColor = cat.color
|
|
305
|
-
catHeader.appendChild(dot)
|
|
306
|
-
catHeader.appendChild(document.createTextNode(cat.name))
|
|
307
|
-
sidebar.appendChild(catHeader)
|
|
308
|
-
|
|
309
|
-
for (const demo of cat.items) {
|
|
310
|
-
const idx = allDemos.indexOf(demo)
|
|
311
|
-
const item = el("div", { className: "vw-item" }, demo.name)
|
|
312
|
-
item.dataset.idx = String(idx)
|
|
313
|
-
item.addEventListener("click", () => {
|
|
314
|
-
selectDemo(idx)
|
|
315
|
-
sidebarFocused = true
|
|
316
|
-
})
|
|
317
|
-
sidebar.appendChild(item)
|
|
318
|
-
sidebarItems[idx] = item
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Terminal pane
|
|
323
|
-
const termWrap = el("div", { className: "vw-term-wrap" })
|
|
324
|
-
const sourceToggle = el("button", { className: "vw-source-toggle" }, "Source")
|
|
325
|
-
termWrap.appendChild(sourceToggle)
|
|
326
|
-
|
|
327
|
-
// Info bar
|
|
328
|
-
const infoDiv = el("div", { className: "vw-info" })
|
|
329
|
-
const infoName = el("div", { className: "vw-info-name" })
|
|
330
|
-
const infoDesc = el("div", { className: "vw-info-desc" })
|
|
331
|
-
const badgesDiv = el("div", { className: "vw-badges" })
|
|
332
|
-
const keyHints = el("div", { className: "vw-keyhints" })
|
|
333
|
-
infoDiv.append(infoName, infoDesc, badgesDiv, keyHints)
|
|
334
|
-
|
|
335
|
-
// Main pane
|
|
336
|
-
const mainPane = el("div", { className: "vw-main" })
|
|
337
|
-
mainPane.append(termWrap, infoDiv)
|
|
338
|
-
|
|
339
|
-
// Source pane
|
|
340
|
-
const sourcePane = el("div", { className: `vw-source${sourceVisible ? "" : " hidden"}` })
|
|
341
|
-
const sourceHeader = el("div", { className: "vw-source-header" })
|
|
342
|
-
const sourceTitle = el("span", { className: "vw-source-title" }, "Source")
|
|
343
|
-
const copyBtn = el("button", { className: "vw-source-btn" }, "Copy")
|
|
344
|
-
sourceHeader.append(sourceTitle, copyBtn)
|
|
345
|
-
const sourceCodeWrap = el("div", { className: "vw-source-code" })
|
|
346
|
-
const sourcePre = el("pre")
|
|
347
|
-
sourceCodeWrap.appendChild(sourcePre)
|
|
348
|
-
sourcePane.append(sourceHeader, sourceCodeWrap)
|
|
349
|
-
|
|
350
|
-
// Body
|
|
351
|
-
const body = el("div", { className: "vw-body" })
|
|
352
|
-
body.append(sidebar, mainPane, sourcePane)
|
|
353
|
-
|
|
354
|
-
// Root
|
|
355
|
-
root.append(header, body)
|
|
356
|
-
|
|
357
|
-
// ─── Terminal setup ────────────────────────────────────────────────
|
|
358
|
-
const term = new Terminal({
|
|
359
|
-
cursorBlink: false,
|
|
360
|
-
convertEol: true,
|
|
361
|
-
cols: 80,
|
|
362
|
-
rows: 24,
|
|
363
|
-
fontFamily: "'Cascadia Code', 'Fira Code', 'JetBrains Mono', Menlo, monospace",
|
|
364
|
-
fontSize: 14,
|
|
365
|
-
theme: {
|
|
366
|
-
background: "#0f0f1a",
|
|
367
|
-
foreground: "#cdd6f4",
|
|
368
|
-
cursor: "#f5e0dc",
|
|
369
|
-
selectionBackground: "rgba(137, 220, 235, 0.25)",
|
|
370
|
-
},
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
const fitAddon = new FitAddon()
|
|
374
|
-
term.loadAddon(fitAddon)
|
|
375
|
-
term.open(termWrap)
|
|
376
|
-
fitAddon.fit()
|
|
377
|
-
|
|
378
|
-
// Track sidebar vs terminal focus for keyboard routing
|
|
379
|
-
term.textarea?.addEventListener("focus", () => {
|
|
380
|
-
sidebarFocused = false
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
// Click terminal to focus it (unfocus sidebar)
|
|
384
|
-
termWrap.addEventListener("click", () => {
|
|
385
|
-
sidebarFocused = false
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
// ─── Selection logic ───────────────────────────────────────────────
|
|
389
|
-
function selectDemo(idx: number): void {
|
|
390
|
-
if (idx < 0 || idx >= allDemos.length) return
|
|
391
|
-
const prev = selectedIdx
|
|
392
|
-
selectedIdx = idx
|
|
393
|
-
|
|
394
|
-
// Update sidebar highlight
|
|
395
|
-
if (sidebarItems[prev]) sidebarItems[prev]!.classList.remove("selected")
|
|
396
|
-
if (sidebarItems[idx]) {
|
|
397
|
-
sidebarItems[idx]!.classList.add("selected")
|
|
398
|
-
sidebarItems[idx]!.scrollIntoView({ block: "nearest" })
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const demo = allDemos[idx]!
|
|
402
|
-
|
|
403
|
-
// Update URL hash for deep linking (without triggering hashchange)
|
|
404
|
-
history.replaceState(null, "", `#${demo.id}`)
|
|
405
|
-
|
|
406
|
-
// Update info
|
|
407
|
-
infoName.textContent = demo.name
|
|
408
|
-
infoDesc.textContent = demo.description
|
|
409
|
-
badgesDiv.innerHTML = ""
|
|
410
|
-
for (const feat of demo.features) {
|
|
411
|
-
badgesDiv.appendChild(el("span", { className: "vw-badge" }, feat))
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Key hints based on whether demo has component
|
|
415
|
-
if (demo.component) {
|
|
416
|
-
keyHints.innerHTML =
|
|
417
|
-
"<kbd>j</kbd><kbd>k</kbd> navigate <kbd>Enter</kbd> select <kbd>s</kbd> toggle source Click terminal for keyboard input"
|
|
418
|
-
} else {
|
|
419
|
-
keyHints.innerHTML =
|
|
420
|
-
"<kbd>j</kbd><kbd>k</kbd> navigate <kbd>s</kbd> toggle source Run in terminal: <code>bun run examples/...</code>"
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Update source pane
|
|
424
|
-
sourcePre.innerHTML = highlightSource(demo.source)
|
|
425
|
-
|
|
426
|
-
// Render demo in terminal
|
|
427
|
-
renderDemo(demo)
|
|
428
|
-
|
|
429
|
-
// Auto-focus terminal so keyboard input works immediately
|
|
430
|
-
term.focus()
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
let pendingRenderFrame: number | null = null
|
|
434
|
-
|
|
435
|
-
function renderDemo(demo: DemoEntry): void {
|
|
436
|
-
// Cancel any pending deferred render from a previous switch
|
|
437
|
-
if (pendingRenderFrame !== null) {
|
|
438
|
-
cancelAnimationFrame(pendingRenderFrame)
|
|
439
|
-
pendingRenderFrame = null
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Cleanup previous — unmount React tree, then fully reset xterm
|
|
443
|
-
if (currentInstance) {
|
|
444
|
-
currentInstance.unmount()
|
|
445
|
-
currentInstance = null
|
|
446
|
-
}
|
|
447
|
-
// Full reset: clear scrollback + visible buffer, reset terminal state
|
|
448
|
-
term.reset()
|
|
449
|
-
|
|
450
|
-
// Defer new render by one frame — ensures any pending requestAnimationFrame
|
|
451
|
-
// callbacks from the old demo's render scheduler have fired (and bailed via
|
|
452
|
-
// their unmounted flag), and any queued xterm.write() calls have been processed.
|
|
453
|
-
pendingRenderFrame = requestAnimationFrame(() => {
|
|
454
|
-
pendingRenderFrame = null
|
|
455
|
-
term.reset() // Clean slate after old async work drained
|
|
456
|
-
|
|
457
|
-
if (!demo.component) {
|
|
458
|
-
term.writeln("\r\n This example requires a full terminal runtime.")
|
|
459
|
-
term.writeln(`\r\n Run: bun run examples/${demo.id}`)
|
|
460
|
-
return
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
fitAddon.fit()
|
|
464
|
-
currentInstance = renderToXterm(React.createElement(demo.component), term, {
|
|
465
|
-
input: true, // enables useInput, useMouse, useTerminalFocused
|
|
466
|
-
handleFocusCycling: false, // showcases handle Tab/Escape themselves
|
|
467
|
-
})
|
|
468
|
-
})
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// ─── Copy button ───────────────────────────────────────────────────
|
|
472
|
-
copyBtn.addEventListener("click", () => {
|
|
473
|
-
const demo = allDemos[selectedIdx]
|
|
474
|
-
if (!demo) return
|
|
475
|
-
void navigator.clipboard.writeText(demo.source).then(() => {
|
|
476
|
-
copyBtn.textContent = "Copied!"
|
|
477
|
-
setTimeout(() => {
|
|
478
|
-
copyBtn.textContent = "Copy"
|
|
479
|
-
}, 1500)
|
|
480
|
-
return undefined
|
|
481
|
-
})
|
|
482
|
-
})
|
|
483
|
-
|
|
484
|
-
// ─── Source toggle ─────────────────────────────────────────────────
|
|
485
|
-
function toggleSource(): void {
|
|
486
|
-
sourceVisible = !sourceVisible
|
|
487
|
-
sourcePane.classList.toggle("hidden", !sourceVisible)
|
|
488
|
-
// Refit terminal after layout change
|
|
489
|
-
requestAnimationFrame(() => {
|
|
490
|
-
fitAddon.fit()
|
|
491
|
-
if (currentInstance) currentInstance.refresh()
|
|
492
|
-
})
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
sourceToggle.addEventListener("click", toggleSource)
|
|
496
|
-
|
|
497
|
-
// ─── Keyboard navigation ──────────────────────────────────────────
|
|
498
|
-
document.addEventListener("keydown", (e: KeyboardEvent) => {
|
|
499
|
-
// Only handle sidebar navigation when sidebar is focused or no terminal focus
|
|
500
|
-
if (sidebarFocused || document.activeElement === document.body) {
|
|
501
|
-
if (e.key === "ArrowDown" || e.key === "j") {
|
|
502
|
-
e.preventDefault()
|
|
503
|
-
selectDemo(Math.min(allDemos.length - 1, selectedIdx + 1))
|
|
504
|
-
} else if (e.key === "ArrowUp" || e.key === "k") {
|
|
505
|
-
e.preventDefault()
|
|
506
|
-
selectDemo(Math.max(0, selectedIdx - 1))
|
|
507
|
-
} else if (e.key === "Enter") {
|
|
508
|
-
e.preventDefault()
|
|
509
|
-
sidebarFocused = false
|
|
510
|
-
term.focus()
|
|
511
|
-
} else if (e.key === "s" || e.key === "S") {
|
|
512
|
-
e.preventDefault()
|
|
513
|
-
toggleSource()
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
})
|
|
517
|
-
|
|
518
|
-
// Click sidebar to focus it
|
|
519
|
-
sidebar.addEventListener("click", () => {
|
|
520
|
-
sidebarFocused = true
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
// ─── Resize handling ───────────────────────────────────────────────
|
|
524
|
-
window.addEventListener("resize", () => {
|
|
525
|
-
fitAddon.fit()
|
|
526
|
-
if (currentInstance) currentInstance.refresh()
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
// ─── Initial selection (from URL hash or default) ──────────────────
|
|
530
|
-
const hashId = window.location.hash.slice(1)
|
|
531
|
-
const hashIdx = hashId ? allDemos.findIndex((d) => d.id === hashId) : -1
|
|
532
|
-
selectDemo(hashIdx >= 0 ? hashIdx : 0)
|
|
533
|
-
|
|
534
|
-
// Handle browser back/forward navigation
|
|
535
|
-
window.addEventListener("hashchange", () => {
|
|
536
|
-
const id = window.location.hash.slice(1)
|
|
537
|
-
const idx = allDemos.findIndex((d) => d.id === id)
|
|
538
|
-
if (idx >= 0 && idx !== selectedIdx) selectDemo(idx)
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
// Signal ready to parent
|
|
542
|
-
window.parent.postMessage({ type: "silvery-ready" }, "*")
|
|
543
|
-
|
|
544
|
-
// Expose for debugging
|
|
545
|
-
;(window as any).silveryViewer = { term, allDemos, selectDemo }
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// =============================================================================
|
|
549
|
-
// Boot
|
|
550
|
-
// =============================================================================
|
|
551
|
-
|
|
552
|
-
const root = document.getElementById("viewer-root")
|
|
553
|
-
if (root) {
|
|
554
|
-
createViewerApp(root)
|
|
555
|
-
}
|
package/examples/web/viewer.html
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>Silvery Examples</title>
|
|
7
|
-
<link rel="stylesheet" href="../../node_modules/@xterm/xterm/css/xterm.css" />
|
|
8
|
-
<link rel="stylesheet" href="./xterm/xterm.css" />
|
|
9
|
-
<style>
|
|
10
|
-
* {
|
|
11
|
-
box-sizing: border-box;
|
|
12
|
-
margin: 0;
|
|
13
|
-
padding: 0;
|
|
14
|
-
}
|
|
15
|
-
html,
|
|
16
|
-
body {
|
|
17
|
-
width: 100%;
|
|
18
|
-
height: 100%;
|
|
19
|
-
overflow: hidden;
|
|
20
|
-
background: #0f0f1a;
|
|
21
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
22
|
-
color: #cdd6f4;
|
|
23
|
-
}
|
|
24
|
-
</style>
|
|
25
|
-
</head>
|
|
26
|
-
<body>
|
|
27
|
-
<div id="viewer-root"></div>
|
|
28
|
-
<script type="module" src="./dist/viewer-app.js"></script>
|
|
29
|
-
</body>
|
|
30
|
-
</html>
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* xterm.js Adapter Demo
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates silvery rendering React components to an xterm.js terminal.
|
|
5
|
-
* The terminal adapter produces ANSI escape sequences, xterm.js renders them.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import React from "react"
|
|
9
|
-
import { Terminal } from "@xterm/xterm"
|
|
10
|
-
import { FitAddon } from "@xterm/addon-fit"
|
|
11
|
-
import { renderToXterm, Box, Text, useContentRect } from "../../packages/term/src/xterm/index.js"
|
|
12
|
-
|
|
13
|
-
// Component that shows its dimensions (in cells, not pixels)
|
|
14
|
-
function SizeDisplay() {
|
|
15
|
-
const { width, height } = useContentRect()
|
|
16
|
-
return (
|
|
17
|
-
<Text color="green">
|
|
18
|
-
Size: {width} cols x {height} rows
|
|
19
|
-
</Text>
|
|
20
|
-
)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Demo component with various styles
|
|
24
|
-
function App() {
|
|
25
|
-
return (
|
|
26
|
-
<Box flexDirection="column" padding={1}>
|
|
27
|
-
<Box borderStyle="single" borderColor="cyan" padding={1}>
|
|
28
|
-
<Box flexDirection="column">
|
|
29
|
-
<Text bold color="cyan">
|
|
30
|
-
silvery xterm.js Rendering
|
|
31
|
-
</Text>
|
|
32
|
-
<SizeDisplay />
|
|
33
|
-
</Box>
|
|
34
|
-
</Box>
|
|
35
|
-
|
|
36
|
-
<Box marginTop={1} borderStyle="round" borderColor="magenta" padding={1}>
|
|
37
|
-
<Box flexDirection="column">
|
|
38
|
-
<Text color="magenta">Text Styles</Text>
|
|
39
|
-
<Box flexDirection="row" gap={2}>
|
|
40
|
-
<Text>Normal</Text>
|
|
41
|
-
<Text bold>Bold</Text>
|
|
42
|
-
<Text italic>Italic</Text>
|
|
43
|
-
</Box>
|
|
44
|
-
<Box flexDirection="row" gap={2}>
|
|
45
|
-
<Text underline>Underline</Text>
|
|
46
|
-
<Text strikethrough>Strike</Text>
|
|
47
|
-
<Text dim>Dim</Text>
|
|
48
|
-
</Box>
|
|
49
|
-
</Box>
|
|
50
|
-
</Box>
|
|
51
|
-
|
|
52
|
-
<Box marginTop={1} flexDirection="row" gap={1}>
|
|
53
|
-
<Box backgroundColor="red" padding={1}>
|
|
54
|
-
<Text color="white">Red</Text>
|
|
55
|
-
</Box>
|
|
56
|
-
<Box backgroundColor="green" padding={1}>
|
|
57
|
-
<Text color="black">Green</Text>
|
|
58
|
-
</Box>
|
|
59
|
-
<Box backgroundColor="blue" padding={1}>
|
|
60
|
-
<Text color="white">Blue</Text>
|
|
61
|
-
</Box>
|
|
62
|
-
</Box>
|
|
63
|
-
|
|
64
|
-
<Box marginTop={1}>
|
|
65
|
-
<Text dim>Layout by Flexx, ANSI output via terminal adapter, rendered by xterm.js</Text>
|
|
66
|
-
</Box>
|
|
67
|
-
</Box>
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Set up xterm.js terminal
|
|
72
|
-
const termContainer = document.getElementById("terminal") as HTMLElement
|
|
73
|
-
if (termContainer) {
|
|
74
|
-
const term = new Terminal({
|
|
75
|
-
cursorBlink: false,
|
|
76
|
-
convertEol: true,
|
|
77
|
-
cols: 80,
|
|
78
|
-
rows: 24,
|
|
79
|
-
fontFamily: "'Cascadia Code', 'Fira Code', 'JetBrains Mono', Menlo, monospace",
|
|
80
|
-
fontSize: 14,
|
|
81
|
-
theme: {
|
|
82
|
-
background: "#1a1a2e",
|
|
83
|
-
foreground: "#eee",
|
|
84
|
-
},
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const fitAddon = new FitAddon()
|
|
88
|
-
term.loadAddon(fitAddon)
|
|
89
|
-
term.open(termContainer)
|
|
90
|
-
fitAddon.fit()
|
|
91
|
-
|
|
92
|
-
const instance = renderToXterm(<App />, term)
|
|
93
|
-
|
|
94
|
-
// Re-fit and re-render on window resize
|
|
95
|
-
// Must use resize() (not refresh()) — clears the old buffer so the
|
|
96
|
-
// next render does a full repaint at the new dimensions.
|
|
97
|
-
window.addEventListener("resize", () => {
|
|
98
|
-
fitAddon.fit()
|
|
99
|
-
instance.resize(term.cols, term.rows)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
// Expose for debugging
|
|
103
|
-
;(window as any).silveryInstance = instance
|
|
104
|
-
;(window as any).xtermTerminal = term
|
|
105
|
-
}
|