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.
Files changed (120) hide show
  1. package/README.md +41 -145
  2. package/dist/chalk.js +3 -0
  3. package/dist/chalk.js.map +11 -0
  4. package/dist/index.js +340 -0
  5. package/dist/index.js.map +282 -0
  6. package/dist/ink.js +129 -0
  7. package/dist/ink.js.map +140 -0
  8. package/dist/runtime.js +394 -0
  9. package/dist/runtime.js.map +286 -0
  10. package/dist/theme.js +343 -0
  11. package/dist/theme.js.map +286 -0
  12. package/dist/ui/animation.js +3 -0
  13. package/dist/ui/animation.js.map +15 -0
  14. package/dist/ui/ansi.js +3 -0
  15. package/dist/ui/ansi.js.map +10 -0
  16. package/dist/ui/cli.js +8 -0
  17. package/dist/ui/cli.js.map +14 -0
  18. package/dist/ui/display.js +4 -0
  19. package/dist/ui/display.js.map +10 -0
  20. package/dist/ui/image.js +4 -0
  21. package/dist/ui/image.js.map +15 -0
  22. package/dist/ui/input.js +3 -0
  23. package/dist/ui/input.js.map +11 -0
  24. package/dist/ui/progress.js +8 -0
  25. package/dist/ui/progress.js.map +20 -0
  26. package/dist/ui/react.js +3 -0
  27. package/dist/ui/react.js.map +15 -0
  28. package/dist/ui/utils.js +3 -0
  29. package/dist/ui/utils.js.map +10 -0
  30. package/dist/ui/wrappers.js +14 -0
  31. package/dist/ui/wrappers.js.map +19 -0
  32. package/dist/ui.js +17 -0
  33. package/dist/ui.js.map +20 -0
  34. package/package.json +67 -15
  35. package/src/index.ts +67 -1
  36. package/src/runtime.ts +4 -0
  37. package/src/theme.ts +4 -0
  38. package/src/ui/animation.ts +2 -0
  39. package/src/ui/ansi.ts +2 -0
  40. package/src/ui/cli.ts +2 -0
  41. package/src/ui/display.ts +2 -0
  42. package/src/ui/image.ts +2 -0
  43. package/src/ui/input.ts +2 -0
  44. package/src/ui/progress.ts +2 -0
  45. package/src/ui/react.ts +2 -0
  46. package/src/ui/utils.ts +2 -0
  47. package/src/ui/wrappers.ts +2 -0
  48. package/src/ui.ts +4 -0
  49. package/examples/CLAUDE.md +0 -75
  50. package/examples/_banner.tsx +0 -60
  51. package/examples/cli.ts +0 -228
  52. package/examples/index.md +0 -101
  53. package/examples/inline/inline-nontty.tsx +0 -98
  54. package/examples/inline/inline-progress.tsx +0 -79
  55. package/examples/inline/inline-simple.tsx +0 -63
  56. package/examples/inline/scrollback.tsx +0 -185
  57. package/examples/interactive/_input-debug.tsx +0 -110
  58. package/examples/interactive/_stdin-test.ts +0 -71
  59. package/examples/interactive/_textarea-bare.tsx +0 -45
  60. package/examples/interactive/aichat/components.tsx +0 -468
  61. package/examples/interactive/aichat/index.tsx +0 -207
  62. package/examples/interactive/aichat/script.ts +0 -460
  63. package/examples/interactive/aichat/state.ts +0 -326
  64. package/examples/interactive/aichat/types.ts +0 -19
  65. package/examples/interactive/app-todo.tsx +0 -198
  66. package/examples/interactive/async-data.tsx +0 -208
  67. package/examples/interactive/cli-wizard.tsx +0 -332
  68. package/examples/interactive/clipboard.tsx +0 -183
  69. package/examples/interactive/components.tsx +0 -463
  70. package/examples/interactive/data-explorer.tsx +0 -506
  71. package/examples/interactive/dev-tools.tsx +0 -379
  72. package/examples/interactive/explorer.tsx +0 -747
  73. package/examples/interactive/gallery.tsx +0 -652
  74. package/examples/interactive/inline-bench.tsx +0 -136
  75. package/examples/interactive/kanban.tsx +0 -267
  76. package/examples/interactive/layout-ref.tsx +0 -185
  77. package/examples/interactive/outline.tsx +0 -171
  78. package/examples/interactive/paste-demo.tsx +0 -198
  79. package/examples/interactive/scroll.tsx +0 -77
  80. package/examples/interactive/search-filter.tsx +0 -240
  81. package/examples/interactive/task-list.tsx +0 -279
  82. package/examples/interactive/terminal.tsx +0 -798
  83. package/examples/interactive/textarea.tsx +0 -103
  84. package/examples/interactive/theme.tsx +0 -336
  85. package/examples/interactive/transform.tsx +0 -256
  86. package/examples/interactive/virtual-10k.tsx +0 -413
  87. package/examples/kitty/canvas.tsx +0 -519
  88. package/examples/kitty/generate-samples.ts +0 -236
  89. package/examples/kitty/image-component.tsx +0 -273
  90. package/examples/kitty/images.tsx +0 -604
  91. package/examples/kitty/input.tsx +0 -371
  92. package/examples/kitty/keys.tsx +0 -378
  93. package/examples/kitty/paint.tsx +0 -1017
  94. package/examples/layout/dashboard.tsx +0 -551
  95. package/examples/layout/live-resize.tsx +0 -290
  96. package/examples/layout/overflow.tsx +0 -51
  97. package/examples/playground/README.md +0 -69
  98. package/examples/playground/build.ts +0 -61
  99. package/examples/playground/index.html +0 -420
  100. package/examples/playground/playground-app.tsx +0 -416
  101. package/examples/runtime/elm-counter.tsx +0 -206
  102. package/examples/runtime/hello-runtime.tsx +0 -73
  103. package/examples/runtime/pipe-composition.tsx +0 -184
  104. package/examples/runtime/run-counter.tsx +0 -78
  105. package/examples/runtime/runtime-counter.tsx +0 -197
  106. package/examples/screenshots/generate.tsx +0 -563
  107. package/examples/scrollback-perf.tsx +0 -230
  108. package/examples/viewer.tsx +0 -654
  109. package/examples/web/build.ts +0 -365
  110. package/examples/web/canvas-app.tsx +0 -80
  111. package/examples/web/canvas.html +0 -89
  112. package/examples/web/dom-app.tsx +0 -81
  113. package/examples/web/dom.html +0 -113
  114. package/examples/web/showcase-app.tsx +0 -107
  115. package/examples/web/showcase.html +0 -34
  116. package/examples/web/showcases/index.tsx +0 -56
  117. package/examples/web/viewer-app.tsx +0 -555
  118. package/examples/web/viewer.html +0 -30
  119. package/examples/web/xterm-app.tsx +0 -105
  120. 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
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 &nbsp; <kbd>Enter</kbd> select &nbsp; <kbd>s</kbd> toggle source &nbsp; Click terminal for keyboard input"
418
- } else {
419
- keyHints.innerHTML =
420
- "<kbd>j</kbd><kbd>k</kbd> navigate &nbsp; <kbd>s</kbd> toggle source &nbsp; 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
- }
@@ -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
- }