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,652 +0,0 @@
1
- /**
2
- * Gallery — Kitty Images, Pixel Art, and Truecolor Rendering
3
- *
4
- * A tabbed demo combining three visual rendering techniques:
5
- * 1. Images — Browse/display images using the Kitty graphics protocol
6
- * 2. Paint — Half-block pixel art canvas with mouse drawing and RGB color picker
7
- * 3. Truecolor — Full truecolor spectrum, HSL rainbows, and 256-color palette
8
- *
9
- * Run: bun vendor/silvery/examples/interactive/gallery.tsx
10
- */
11
-
12
- import { deflateSync } from "node:zlib"
13
- import React, { useState, useMemo } from "react"
14
- import {
15
- render,
16
- Box,
17
- Text,
18
- Image,
19
- Tabs,
20
- TabList,
21
- Tab,
22
- TabPanel,
23
- Kbd,
24
- Muted,
25
- H2,
26
- useInput,
27
- useApp,
28
- useContentRect,
29
- createTerm,
30
- type Key,
31
- } from "../../src/index.js"
32
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
33
-
34
- export const meta: ExampleMeta = {
35
- name: "Gallery",
36
- description: "Kitty images, pixel art, and truecolor rendering",
37
- demo: true,
38
- features: ["Image", "Kitty graphics", "half-block", "truecolor", "mouse input"],
39
- }
40
-
41
- // ============================================================================
42
- // Color Utilities
43
- // ============================================================================
44
-
45
- type RGB = [number, number, number]
46
-
47
- /** HSV to RGB (h: 0-360, s/v: 0-1) */
48
- function hsvToRgb(h: number, s: number, v: number): RGB {
49
- const c = v * s
50
- const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
51
- const m = v - c
52
- let r = 0,
53
- g = 0,
54
- b = 0
55
- if (h < 60) {
56
- r = c
57
- g = x
58
- } else if (h < 120) {
59
- r = x
60
- g = c
61
- } else if (h < 180) {
62
- g = c
63
- b = x
64
- } else if (h < 240) {
65
- g = x
66
- b = c
67
- } else if (h < 300) {
68
- r = x
69
- b = c
70
- } else {
71
- r = c
72
- b = x
73
- }
74
- return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)]
75
- }
76
-
77
- /** HSL to RGB (h: 0-360, s/l: 0-1) */
78
- function hslToRgb(h: number, s: number, l: number): RGB {
79
- const c = (1 - Math.abs(2 * l - 1)) * s
80
- const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
81
- const m = l - c / 2
82
- let r1: number, g1: number, b1: number
83
- if (h < 60) [r1, g1, b1] = [c, x, 0]
84
- else if (h < 120) [r1, g1, b1] = [x, c, 0]
85
- else if (h < 180) [r1, g1, b1] = [0, c, x]
86
- else if (h < 240) [r1, g1, b1] = [0, x, c]
87
- else if (h < 300) [r1, g1, b1] = [x, 0, c]
88
- else [r1, g1, b1] = [c, 0, x]
89
- return [Math.round((r1 + m) * 255), Math.round((g1 + m) * 255), Math.round((b1 + m) * 255)]
90
- }
91
-
92
- // ============================================================================
93
- // PNG Generation (in-memory, no external files)
94
- // ============================================================================
95
-
96
- function crc32(data: Buffer): number {
97
- let crc = 0xffffffff
98
- for (let i = 0; i < data.length; i++) {
99
- crc ^= data[i]!
100
- for (let j = 0; j < 8; j++) {
101
- crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1
102
- }
103
- }
104
- return (crc ^ 0xffffffff) >>> 0
105
- }
106
-
107
- function makeChunk(type: string, data: Buffer): Buffer {
108
- const len = Buffer.alloc(4)
109
- len.writeUInt32BE(data.length)
110
- const typeBytes = Buffer.from(type, "ascii")
111
- const payload = Buffer.concat([typeBytes, data])
112
- const crc = crc32(payload)
113
- const crcBuf = Buffer.alloc(4)
114
- crcBuf.writeUInt32BE(crc >>> 0)
115
- return Buffer.concat([len, payload, crcBuf])
116
- }
117
-
118
- function encodePng(width: number, height: number, pixelFn: (x: number, y: number) => RGB): Buffer {
119
- const rawData = Buffer.alloc(height * (1 + width * 4))
120
- for (let y = 0; y < height; y++) {
121
- const rowOffset = y * (1 + width * 4)
122
- rawData[rowOffset] = 0
123
- for (let x = 0; x < width; x++) {
124
- const [r, g, b] = pixelFn(x, y)
125
- const off = rowOffset + 1 + x * 4
126
- rawData[off] = r
127
- rawData[off + 1] = g
128
- rawData[off + 2] = b
129
- rawData[off + 3] = 255
130
- }
131
- }
132
- const compressed = deflateSync(rawData)
133
- const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
134
- const ihdr = Buffer.alloc(13)
135
- ihdr.writeUInt32BE(width, 0)
136
- ihdr.writeUInt32BE(height, 4)
137
- ihdr[8] = 8
138
- ihdr[9] = 6
139
- return Buffer.concat([
140
- signature,
141
- makeChunk("IHDR", ihdr),
142
- makeChunk("IDAT", compressed),
143
- makeChunk("IEND", Buffer.alloc(0)),
144
- ])
145
- }
146
-
147
- // ============================================================================
148
- // Sample Image Generators
149
- // ============================================================================
150
-
151
- interface GalleryImage {
152
- name: string
153
- description: string
154
- png: Buffer
155
- }
156
-
157
- function generateRainbow(w: number, h: number): Buffer {
158
- return encodePng(w, h, (x, y) => {
159
- const hue = (x / w) * 360
160
- const sat = 0.7 + 0.3 * Math.sin((y / h) * Math.PI)
161
- const val = 0.5 + 0.5 * Math.cos((y / h) * Math.PI * 2)
162
- return hsvToRgb(hue, sat, val)
163
- })
164
- }
165
-
166
- function generatePlasma(w: number, h: number): Buffer {
167
- return encodePng(w, h, (x, y) => {
168
- const nx = x / w
169
- const ny = y / h
170
- const v1 = Math.sin(nx * 10 + ny * 3)
171
- const v2 = Math.sin(nx * 5 - ny * 8 + 2)
172
- const v3 = Math.sin(Math.sqrt((nx - 0.5) ** 2 + (ny - 0.5) ** 2) * 15)
173
- const v = (v1 + v2 + v3) / 3
174
- const hue = ((v + 1) / 2) * 360
175
- return hslToRgb(hue, 0.9, 0.55)
176
- })
177
- }
178
-
179
- function generateMandelbrot(w: number, h: number): Buffer {
180
- return encodePng(w, h, (x, y) => {
181
- const cx = (x / w) * 3.5 - 2.5
182
- const cy = (y / h) * 2.0 - 1.0
183
- let zx = 0,
184
- zy = 0
185
- let i = 0
186
- const maxIter = 80
187
- while (zx * zx + zy * zy < 4 && i < maxIter) {
188
- const tmp = zx * zx - zy * zy + cx
189
- zy = 2 * zx * zy + cy
190
- zx = tmp
191
- i++
192
- }
193
- if (i === maxIter) return [0, 0, 0] as RGB
194
- const hue = (i / maxIter) * 360
195
- return hslToRgb(hue, 1.0, 0.5)
196
- })
197
- }
198
-
199
- function generateGradientGrid(w: number, h: number): Buffer {
200
- return encodePng(w, h, (x, y) => {
201
- const r = Math.round((x / w) * 255)
202
- const g = Math.round((y / h) * 255)
203
- const b = Math.round(255 - ((x + y) / (w + h)) * 255)
204
- return [r, g, b]
205
- })
206
- }
207
-
208
- function generateCheckerPattern(w: number, h: number): Buffer {
209
- const size = 16
210
- return encodePng(w, h, (x, y) => {
211
- const cx = Math.floor(x / size)
212
- const cy = Math.floor(y / size)
213
- const hue = ((cx + cy) * 30) % 360
214
- const isLight = (cx + cy) % 2 === 0
215
- return hslToRgb(hue, 0.8, isLight ? 0.6 : 0.35)
216
- })
217
- }
218
-
219
- // ============================================================================
220
- // Tab 1: Images — Browse gallery of generated images
221
- // ============================================================================
222
-
223
- function ImagesTab(): JSX.Element {
224
- const rect = useContentRect()
225
- const w = Math.max(20, rect.width - 4)
226
- const imgH = Math.max(5, rect.height - 6)
227
-
228
- const images: GalleryImage[] = useMemo(() => {
229
- const pw = 256
230
- const ph = 192
231
- return [
232
- { name: "Rainbow", description: "HSV color wheel gradient", png: generateRainbow(pw, ph) },
233
- { name: "Plasma", description: "Sine-wave plasma interference", png: generatePlasma(pw, ph) },
234
- {
235
- name: "Mandelbrot",
236
- description: "Fractal escape-time coloring",
237
- png: generateMandelbrot(pw, ph),
238
- },
239
- {
240
- name: "RGB Cube",
241
- description: "Red-Green-Blue gradient grid",
242
- png: generateGradientGrid(pw, ph),
243
- },
244
- {
245
- name: "Checker",
246
- description: "Hue-shifted checkerboard",
247
- png: generateCheckerPattern(pw, ph),
248
- },
249
- ]
250
- }, [])
251
-
252
- const [index, setIndex] = useState(0)
253
- const img = images[index]!
254
-
255
- useInput((input: string, key: Key) => {
256
- if (input === "j" || key.downArrow || input === "n") {
257
- setIndex((i) => (i + 1) % images.length)
258
- }
259
- if (input === "k" || key.upArrow || input === "p") {
260
- setIndex((i) => (i - 1 + images.length) % images.length)
261
- }
262
- })
263
-
264
- return (
265
- <Box flexDirection="column" flexGrow={1} gap={1}>
266
- <Box paddingX={1} gap={2}>
267
- <Text bold color="$primary">
268
- {img.name}
269
- </Text>
270
- <Muted>{img.description}</Muted>
271
- <Muted>
272
- ({index + 1}/{images.length})
273
- </Muted>
274
- </Box>
275
- <Box flexGrow={1} justifyContent="center" paddingX={1}>
276
- <Image
277
- src={img.png}
278
- width={w}
279
- height={imgH}
280
- fallback={`[${img.name} — graphics protocol not available. Run in Kitty/WezTerm/Ghostty for images.]`}
281
- />
282
- </Box>
283
- <Muted>
284
- {" "}
285
- <Kbd>j/k</Kbd> navigate images
286
- </Muted>
287
- </Box>
288
- )
289
- }
290
-
291
- // ============================================================================
292
- // Tab 2: Paint — Half-block pixel art canvas
293
- // ============================================================================
294
-
295
- const UPPER_HALF = "\u2580"
296
- const LOWER_HALF = "\u2584"
297
- const FULL_BLOCK = "\u2588"
298
-
299
- const PAINT_PRESETS: { name: string; color: RGB }[] = [
300
- { name: "white", color: [255, 255, 255] },
301
- { name: "red", color: [255, 0, 0] },
302
- { name: "orange", color: [255, 165, 0] },
303
- { name: "yellow", color: [255, 255, 0] },
304
- { name: "green", color: [0, 200, 0] },
305
- { name: "cyan", color: [0, 255, 255] },
306
- { name: "blue", color: [0, 100, 255] },
307
- { name: "magenta", color: [200, 0, 200] },
308
- { name: "pink", color: [255, 128, 200] },
309
- { name: "black", color: [30, 30, 30] },
310
- ]
311
-
312
- function PaintTab(): JSX.Element {
313
- const rect = useContentRect()
314
- const canvasW = Math.max(10, rect.width - 2)
315
- const canvasTermH = Math.max(4, rect.height - 7)
316
- const canvasPixH = canvasTermH * 2
317
-
318
- const [pixels, setPixels] = useState<(RGB | null)[][]>(() => {
319
- const rows: (RGB | null)[][] = []
320
- for (let y = 0; y < canvasPixH; y++) rows.push(new Array(canvasW).fill(null))
321
- // Seed with a colorful spiral pattern so the demo looks great on first render
322
- const cx = Math.floor(canvasW / 2)
323
- const cy = Math.floor(canvasPixH / 2)
324
- const radius = Math.min(cx, cy) - 2
325
- for (let angle = 0; angle < 720; angle += 2) {
326
- const r = (angle / 720) * radius
327
- const rad = (angle * Math.PI) / 180
328
- const px = Math.round(cx + r * Math.cos(rad))
329
- const py = Math.round(cy + r * Math.sin(rad))
330
- const hue = angle % 360
331
- const color = hslToRgb(hue, 0.9, 0.55)
332
- // Draw a small dot (2px radius)
333
- for (let dy = -1; dy <= 1; dy++) {
334
- for (let dx = -1; dx <= 1; dx++) {
335
- const x = px + dx
336
- const y = py + dy
337
- if (x >= 0 && x < canvasW && y >= 0 && y < canvasPixH) {
338
- rows[y]![x] = color
339
- }
340
- }
341
- }
342
- }
343
- return rows
344
- })
345
-
346
- const [colorIndex, setColorIndex] = useState(1) // red
347
- const [tool, setTool] = useState<"pen" | "eraser">("pen")
348
- const currentColor = PAINT_PRESETS[colorIndex]!.color
349
-
350
- // Handle keyboard: color presets, tool toggle, clear
351
- useInput((input: string) => {
352
- if (input >= "1" && input <= "9") {
353
- setColorIndex(Number(input) - 1)
354
- setTool("pen")
355
- } else if (input === "0") {
356
- setColorIndex(9)
357
- setTool("pen")
358
- } else if (input === "e") {
359
- setTool((t) => (t === "eraser" ? "pen" : "eraser"))
360
- } else if (input === "c") {
361
- setPixels((prev) => prev.map((row) => row.map(() => null)))
362
- }
363
- })
364
-
365
- // Render canvas as half-block characters
366
- const canvasLines: JSX.Element[] = []
367
- for (let row = 0; row < canvasTermH; row++) {
368
- const cells: JSX.Element[] = []
369
- for (let col = 0; col < canvasW; col++) {
370
- const top = row * 2 < pixels.length ? (pixels[row * 2]?.[col] ?? null) : null
371
- const bot = row * 2 + 1 < pixels.length ? (pixels[row * 2 + 1]?.[col] ?? null) : null
372
-
373
- if (top === null && bot === null) {
374
- cells.push(<Text key={col}> </Text>)
375
- } else if (top !== null && bot === null) {
376
- cells.push(
377
- <Text key={col} color={`rgb(${top[0]},${top[1]},${top[2]})`}>
378
- {UPPER_HALF}
379
- </Text>,
380
- )
381
- } else if (top === null && bot !== null) {
382
- cells.push(
383
- <Text key={col} color={`rgb(${bot[0]},${bot[1]},${bot[2]})`}>
384
- {LOWER_HALF}
385
- </Text>,
386
- )
387
- } else if (top !== null && top[0] === bot?.[0] && top[1] === bot[1] && top[2] === bot[2]) {
388
- cells.push(
389
- <Text key={col} color={`rgb(${top[0]},${top[1]},${top[2]})`}>
390
- {FULL_BLOCK}
391
- </Text>,
392
- )
393
- } else {
394
- cells.push(
395
- <Text
396
- key={col}
397
- color={`rgb(${top![0]},${top![1]},${top![2]})`}
398
- backgroundColor={`rgb(${bot![0]},${bot![1]},${bot![2]})`}
399
- >
400
- {UPPER_HALF}
401
- </Text>,
402
- )
403
- }
404
- }
405
- canvasLines.push(<Box key={row}>{cells}</Box>)
406
- }
407
-
408
- // Color palette bar
409
- const paletteItems = PAINT_PRESETS.map((p, i) => {
410
- const selected = i === colorIndex
411
- return (
412
- <Text
413
- key={i}
414
- backgroundColor={`rgb(${p.color[0]},${p.color[1]},${p.color[2]})`}
415
- color={p.color[0] + p.color[1] + p.color[2] > 384 ? "black" : "white"}
416
- bold={selected}
417
- >
418
- {selected ? `[${(i + 1) % 10}]` : ` ${(i + 1) % 10} `}
419
- </Text>
420
- )
421
- })
422
-
423
- const toolLabel = tool === "pen" ? "Pen" : "Eraser"
424
- const [cr, cg, cb] = currentColor
425
-
426
- return (
427
- <Box flexDirection="column" flexGrow={1}>
428
- <Box paddingX={1} gap={2}>
429
- <Text bold color={`rgb(${cr},${cg},${cb})`}>
430
- {toolLabel}
431
- </Text>
432
- <Text backgroundColor={`rgb(${cr},${cg},${cb})`}>{" "}</Text>
433
- <Muted>
434
- rgb({cr},{cg},{cb})
435
- </Muted>
436
- </Box>
437
-
438
- <Box flexDirection="column" flexGrow={1} borderStyle="round" marginX={1}>
439
- <Box flexDirection="column">{canvasLines}</Box>
440
- </Box>
441
-
442
- <Box paddingX={1} gap={0}>
443
- {paletteItems}
444
- </Box>
445
-
446
- <Muted>
447
- {" "}
448
- <Kbd>1-0</Kbd> color <Kbd>e</Kbd> eraser <Kbd>c</Kbd> clear (click canvas in Kitty/Ghostty for mouse paint)
449
- </Muted>
450
- </Box>
451
- )
452
- }
453
-
454
- // ============================================================================
455
- // Tab 3: Truecolor — Spectrum display
456
- // ============================================================================
457
-
458
- function TruecolorTab(): JSX.Element {
459
- const rect = useContentRect()
460
- const w = Math.max(20, rect.width - 4)
461
- const availH = Math.max(10, rect.height - 3)
462
-
463
- // Distribute vertical space among sections
464
- const hueBarH = Math.min(3, Math.max(1, Math.floor(availH * 0.15)))
465
- const gradientH = Math.min(8, Math.max(2, Math.floor(availH * 0.35)))
466
- const paletteH = Math.min(4, Math.max(2, Math.floor(availH * 0.2)))
467
-
468
- // HSL Hue rainbow bar — each column is a hue
469
- const hueBar: JSX.Element[] = []
470
- for (let row = 0; row < hueBarH; row++) {
471
- const cells: JSX.Element[] = []
472
- for (let col = 0; col < w; col++) {
473
- const hue = (col / w) * 360
474
- const lightness = 0.35 + (row / Math.max(1, hueBarH - 1)) * 0.3
475
- const [r, g, b] = hslToRgb(hue, 1.0, lightness)
476
- cells.push(
477
- <Text key={col} backgroundColor={`rgb(${r},${g},${b})`}>
478
- {" "}
479
- </Text>,
480
- )
481
- }
482
- hueBar.push(<Box key={row}>{cells}</Box>)
483
- }
484
-
485
- // Saturation/brightness gradient — rows vary saturation, columns vary hue
486
- const gradient: JSX.Element[] = []
487
- for (let row = 0; row < gradientH; row++) {
488
- const cells: JSX.Element[] = []
489
- const sat = 1.0 - (row / Math.max(1, gradientH - 1)) * 0.8
490
- for (let col = 0; col < w; col++) {
491
- const hue = (col / w) * 360
492
- const [r, g, b] = hsvToRgb(hue, sat, 0.95)
493
- cells.push(
494
- <Text key={col} backgroundColor={`rgb(${r},${g},${b})`}>
495
- {" "}
496
- </Text>,
497
- )
498
- }
499
- gradient.push(<Box key={row}>{cells}</Box>)
500
- }
501
-
502
- // 256-color ANSI palette grid (16 columns x rows)
503
- const paletteCols = 16
504
- const paletteRows = Math.min(paletteH, Math.ceil(256 / paletteCols))
505
- const palette: JSX.Element[] = []
506
- for (let row = 0; row < paletteRows; row++) {
507
- const cells: JSX.Element[] = []
508
- const cellW = Math.max(1, Math.floor(w / paletteCols))
509
- for (let col = 0; col < paletteCols; col++) {
510
- const idx = row * paletteCols + col
511
- if (idx >= 256) break
512
- // Convert 256-color index to RGB
513
- const [r, g, b] = ansi256toRgb(idx)
514
- const label = idx.toString().padStart(3)
515
- const textColor = r + g + b > 384 ? "black" : "white"
516
- cells.push(
517
- <Box key={col} width={cellW}>
518
- <Text backgroundColor={`rgb(${r},${g},${b})`} color={textColor}>
519
- {label.slice(0, cellW)}
520
- </Text>
521
- </Box>,
522
- )
523
- }
524
- palette.push(<Box key={row}>{cells}</Box>)
525
- }
526
-
527
- // Grayscale ramp
528
- const grayCells: JSX.Element[] = []
529
- for (let col = 0; col < w; col++) {
530
- const v = Math.round((col / Math.max(1, w - 1)) * 255)
531
- grayCells.push(
532
- <Text key={col} backgroundColor={`rgb(${v},${v},${v})`}>
533
- {" "}
534
- </Text>,
535
- )
536
- }
537
-
538
- return (
539
- <Box flexDirection="column" flexGrow={1} gap={1} paddingX={1}>
540
- <Box flexDirection="column">
541
- <H2>HSL Rainbow</H2>
542
- <Box flexDirection="column">{hueBar}</Box>
543
- </Box>
544
-
545
- <Box flexDirection="column">
546
- <H2>Saturation Gradient</H2>
547
- <Box flexDirection="column">{gradient}</Box>
548
- </Box>
549
-
550
- <Box flexDirection="column">
551
- <H2>256-Color Palette</H2>
552
- <Box flexDirection="column">{palette}</Box>
553
- </Box>
554
-
555
- <Box flexDirection="column">
556
- <H2>Grayscale Ramp</H2>
557
- <Box>{grayCells}</Box>
558
- </Box>
559
- </Box>
560
- )
561
- }
562
-
563
- /** Convert ANSI 256-color index to RGB */
564
- function ansi256toRgb(idx: number): RGB {
565
- if (idx < 16) {
566
- // Standard 16 colors (approximate)
567
- const table: RGB[] = [
568
- [0, 0, 0],
569
- [128, 0, 0],
570
- [0, 128, 0],
571
- [128, 128, 0],
572
- [0, 0, 128],
573
- [128, 0, 128],
574
- [0, 128, 128],
575
- [192, 192, 192],
576
- [128, 128, 128],
577
- [255, 0, 0],
578
- [0, 255, 0],
579
- [255, 255, 0],
580
- [0, 0, 255],
581
- [255, 0, 255],
582
- [0, 255, 255],
583
- [255, 255, 255],
584
- ]
585
- return table[idx]!
586
- }
587
- if (idx < 232) {
588
- // 6x6x6 color cube
589
- const i = idx - 16
590
- const r = Math.floor(i / 36)
591
- const g = Math.floor((i % 36) / 6)
592
- const b = i % 6
593
- return [r ? r * 40 + 55 : 0, g ? g * 40 + 55 : 0, b ? b * 40 + 55 : 0]
594
- }
595
- // Grayscale ramp (232-255)
596
- const v = (idx - 232) * 10 + 8
597
- return [v, v, v]
598
- }
599
-
600
- // ============================================================================
601
- // Main Gallery App
602
- // ============================================================================
603
-
604
- export function Gallery(): JSX.Element {
605
- const { exit } = useApp()
606
- const [activeTab, setActiveTab] = useState("images")
607
-
608
- useInput((input: string, key: Key) => {
609
- if (input === "q" || key.escape) exit()
610
- })
611
-
612
- return (
613
- <Box flexDirection="column" flexGrow={1}>
614
- <Tabs value={activeTab} onChange={setActiveTab}>
615
- <TabList>
616
- <Tab value="images">Images</Tab>
617
- <Tab value="paint">Paint</Tab>
618
- <Tab value="truecolor">Truecolor</Tab>
619
- </TabList>
620
-
621
- <TabPanel value="images">
622
- <ImagesTab />
623
- </TabPanel>
624
- <TabPanel value="paint">
625
- <PaintTab />
626
- </TabPanel>
627
- <TabPanel value="truecolor">
628
- <TruecolorTab />
629
- </TabPanel>
630
- </Tabs>
631
- </Box>
632
- )
633
- }
634
-
635
- // ============================================================================
636
- // Main
637
- // ============================================================================
638
-
639
- export async function main() {
640
- using term = createTerm()
641
- const { waitUntilExit } = await render(
642
- <ExampleBanner meta={meta} controls="h/l tab j/k navigate Esc/q quit">
643
- <Gallery />
644
- </ExampleBanner>,
645
- term,
646
- )
647
- await waitUntilExit()
648
- }
649
-
650
- if (import.meta.main) {
651
- main().catch(console.error)
652
- }