@silvery/examples 0.17.3 → 0.17.4

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