Opens a pane that draws every mod element

A Claude Code mod that adds a /gallery command. The command opens one pane that draws every element a mod can draw, so you can see each one in your own terminal and theme.
git clone https://github.com/shelmecha/claude-mods-gallery
claude --plugin-dir ./claude-mods-gallery
At the Claude Code prompt, run /gallery. Press Esc to close the pane. Use a wide, fullscreen terminal and scroll inside the pane.
Text styles: bold, italic, underline, strikethrough, dim, inverse, wrap, theme colorsMarkdown, Link, Code and a diffBox with all 10 border stylesButton, Input and SelectRaster, Image, Client and SvgRaster and Image use fixed colors.Image draws in kitty 0.28+ and Ghostty, and not inside tmux. Other terminals show dim alt text.Svg draws in the Desktop app only. The terminal shows a note.claude plugin validate .MIT
hooks/register.js 108 lines1const noop = () => {}
2let picked = 'md'
3
4const DEFAULT_COLOR = 0x01000000
5function cellsOf(rows) {
6 const numbers = rows.flat().flatMap(([char, color]) => [char.codePointAt(0), color, DEFAULT_COLOR])
7 return new Uint8Array(Uint32Array.from(numbers).buffer).toBase64()
8}
9
10// 32 by 8 RGBA pixels, blue on the left fading to red on the right
11function gradient() {
12 const px = new Uint8Array(32 * 8 * 4)
13 for (let y = 0; y < 8; y++) for (let x = 0; x < 32; x++) {
14 const i = (y * 32 + x) * 4
15 px[i] = Math.round((x / 31) * 255); px[i + 1] = 40; px[i + 2] = Math.round((1 - x / 31) * 255); px[i + 3] = 255
16 }
17 return px.toBase64()
18}
19
20export function register(on) {
21 on('session.start', async ($, e, next) => {
22 await $.command.register({ name: 'gallery', description: 'Show every mod element' })
23 return next(e)
24 })
25
26 on('command.run', { command: 'gallery' }, async ($) => {
27 await $.ui.open({ id: 'gallery', focus: true, closeOnEscape: true })
28 return {}
29 })
30
31 on('ui.render', { component: 'Pane' }, async ($, e, next) => {
32 if (e.requestId !== 'gallery') return next(e)
33 const { Box, Text, Button, Input, Select, Link, Markdown, Code, Raster, Image, Svg, Client } = $.ui.resolve(e)
34 const head = (t) => Text({ bold: true, color: 'claude', children: [t] })
35 return Box({
36 flexDirection: 'column',
37 gap: 1,
38 children: [
39 Box({ flexDirection: 'column', children: [
40 head('TEXT STYLES'),
41 Text({ children: ['plain'] }),
42 Text({ bold: true, children: ['bold'] }),
43 Text({ italic: true, children: ['italic'] }),
44 Text({ underline: true, children: ['underline'] }),
45 Text({ strikethrough: true, children: ['strikethrough'] }),
46 Text({ dimColor: true, children: ['dim'] }),
47 Text({ inverse: true, children: ['inverse'] }),
48 Text({ color: 'warning', children: ["theme key: warning"] }),
49 Text({ color: 'suggestion', children: ['theme key: suggestion'] }),
50 Text({ color: 'permission', children: ['theme key: permission'] }),
51 Text({ color: 'fastMode', children: ['theme key: fastMode'] }),
52 Text({ color: 'inactive', children: ['theme key: inactive'] }),
53 Text({ backgroundColor: 'userMessageBackground', children: ['theme background: userMessageBackground'] }),
54 Text({ wrap: 'truncate-end', children: ['wrap truncate-end: this long line is cut off at the pane edge so it never wraps onto a second row'] }),
55 ] }),
56 Box({ flexDirection: 'column', children: [
57 head('MARKDOWN AND LINK'),
58 Markdown({ text: '## Heading\n\nSome **bold** and `code`:\n\n- one\n- two\n\n> a quote' }),
59 Link({ href: 'https://code.claude.com/docs', label: 'Claude Code docs' }),
60 ] }),
61 Box({ flexDirection: 'column', children: [
62 head('CODE AND DIFF'),
63 Code({ language: 'javascript', startLine: 1, source: "const name = 'mods'\nconsole.log('hello ' + name)" }),
64 Code({ format: 'diff', source: '@@ -1,3 +1,3 @@\n # Mods\n-A mod is a plugin.\n+A mod is a plugin that runs code.\n Read on.' }),
65 ] }),
66 Box({ flexDirection: 'column', children: [
67 head('BOX BORDERS'),
68 ...['single', 'double', 'round', 'bold', 'singleDouble', 'doubleSingle', 'classic', 'arrow', 'dashed', 'quote'].map((s) =>
69 Box({ borderStyle: s, paddingX: 1, children: [Text({ children: [`borderStyle: '${s}'`] })] })),
70 ] }),
71 Box({ flexDirection: 'column', children: [
72 head('CONTROLS (Tab moves between them)'),
73 Button({ key: 'save', label: 'Save', onPress: noop }),
74 Button({ key: 'next', label: 'Next', hotkey: 'n', plain: true, onPress: noop }),
75 Button({ key: 'skip', label: 'Skip', dimColor: true, onPress: noop }),
76 Input({ key: 'title', label: 'Title', placeholder: 'Type and press Enter', value: '', submitLabel: 'save', onSubmit: noop }),
77 Select({ key: 'format', label: 'Format', value: picked,
78 options: [{ value: 'md', label: 'Markdown' }, { value: 'html', label: 'HTML' }, { value: 'txt', label: 'Plain text' }],
79 onSelect: (v) => { picked = v } }),
80 ] }),
81 Box({ flexDirection: 'column', children: [
82 head('RASTER (colored cells)'),
83 Raster({ key: 'grid', columns: 3, rows: 2, cells: cellsOf([
84 [['█', 0x2e7d32], ['█', 0xf9a825], ['█', 0xc62828]],
85 [['█', 0x2e7d32], ['█', 0x2e7d32], ['█', 0xf9a825]],
86 ]) }),
87 ] }),
88 Box({ flexDirection: 'column', children: [
89 head('IMAGE (kitty or Ghostty terminals only; else dim alt text)'),
90 Image({ key: 'pic', columns: 16, rows: 4, alt: 'a blue to red gradient',
91 source: { rgba: gradient(), width: 32, height: 8 } }),
92 ] }),
93 Box({ flexDirection: 'column', children: [
94 head('CLIENT (own module, ticks twice a second)'),
95 Client({ key: 'ticker', module: './client/ticker.js', props: { label: 'ticks' } }),
96 ] }),
97 Box({ flexDirection: 'column', children: [
98 head('SVG (Desktop app only)'),
99 e.surface === 'desktop'
100 ? Svg({ alt: 'three bars', width: 120, height: 60,
101 source: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 60"><rect x="10" y="40" width="20" height="20" fill="#2e7d32"/><rect x="50" y="25" width="20" height="35" fill="#f9a825"/><rect x="90" y="5" width="20" height="55" fill="#c62828"/></svg>' })
102 : Text({ dimColor: true, children: ['Svg draws in the Desktop app, not the terminal'] }),
103 ] }),
104 ],
105 })
106 })
107}
108hooks/client/ticker.js 16 lines1// A Client module: it draws a counter that ticks twice a second, with no hooks module involved
2let ticks = 0
3export default function Ticker(props, s) {
4 const { Box, Text } = s.elements
5 if (s.state === undefined) {
6 // First call only: start the timer, then store the first value
7 s.every(500, () => { ticks += 1; s.setState(ticks) })
8 s.setState(0)
9 }
10 return Box({
11 borderStyle: 'round',
12 paddingX: 1,
13 children: [Text({ color: 'fastMode', children: [props.label + ': ' + (s.state ?? 0)] })],
14 })
15}
16