SLOPSHOPPER

gallery

Opens a pane that draws every mod element

newpanecommand
v0.1.0MITupdated 2026-10-09shelmecha/claude-mods-gallery
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · gallery
│ ┃ gallery ✕ › fix the failing auth test and add an audit log call │ ┃ TEXT STYLES │ ┃ plain ⏺ Read(src/auth.ts) │ ┃ bold ⎿ Read 6 lines │ ┃ italic ⏺ Update(src/auth.ts) │ ┃ underline ⎿ Added 2 lines, removed 1 line │ ┃ strikethrough ⏺ Bash(bun test) │ ┃ dim ⎿ 3 pass, 1 fail │ ┃ inverse │ ┃ theme key: warning ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ theme key: suggestion │ ┃ theme key: permission ✻ Worked for 42s · done 4:20 PM │ ┃ theme key: fastMode │ ┃ theme key: inactive › /gallery │ ┃ theme background: userMessageBackground │ ┃ wrap truncate-end: this long line is cut of… │ ┃ │ ┃ MARKDOWN AND LINK │ ┃ Heading │ ┃ │ ┃ Some bold and code: │ ┃ │ ┃ • one │ ┃ • two │ ┃ │ ┃ ▎ a quote ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · gallery
TEXT STYLES plain bold italic underline strikethrough dim inverse theme key: warning theme key: suggestion theme key: permission theme key: fastMode theme key: inactive theme background: userMessageBackground wrap truncate-end: this long line is cut off at the pane ed… MARKDOWN AND LINK Heading Some bold and code: • one • two ▎ a quote Claude Code docs https://code.claude.com/docs CODE AND DIFF 1 const name = 'mods' 2 console.log('hello ' + name) 1 # Mods 2 -A mod is a plugin. 2 +A mod is a plugin that runs code. 3 Read on. BOX BORDERS ┌──────────────────────────────────────────────────────────┐ │ borderStyle: 'single' │ └──────────────────────────────────────────────────────────┘ ╔══════════════════════════════════════════════════════════╗ ║ borderStyle: 'double' ║ ╚══════════════════════════════════════════════════════════╝ ╭──────────────────────────────────────────────────────────╮ │ borderStyle: 'round' │ ╰──────────────────────────────────────────────────────────╯ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ borderStyle: 'bold' ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ╓──────────────────────────────────────────────────────────╖ ║ borderStyle: 'singleDouble' ║ ╙──────────────────────────────────────────────────────────╜ ╒══════════════════════════════════════════════════════════╕ │ borderStyle: 'doubleSingle' │ ╘══════════════════════════════════════════════════════════╛ +----------------------------------------------------------+ | borderStyle: 'classic' | +----------------------------------------------------------+ ↘→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→↙ ↓ borderStyle: 'arrow' ↓ ↗→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→↖ ┌──────────────────────────────────────────────────────────┐ │ borderStyle: 'dashed' │ └──────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────┐ │ borderStyle: 'quote' │ └──────────────────────────────────────────────────────────┘ CONTROLS (Tab moves between them) [ Save ] n: Next [ Skip ] Title: Type and press Enter ⏎ save Format: Markdown ▾ RASTER (colored cells) ███ ███ IMAGE (kitty or Ghostty terminals only; else dim alt text) ▣ a blue to red gradient CLIENT (own module, ticks twice a second) ▣ client module ./client/ticker.js SVG (Desktop app only) Svg draws in the Desktop app, not the terminal
README

Claude Mods Gallery

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.

Run it

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.

What the pane shows

  • Text styles: bold, italic, underline, strikethrough, dim, inverse, wrap, theme colors
  • Markdown, Link, Code and a diff
  • Box with all 10 border styles
  • Button, Input and Select
  • Raster, Image, Client and Svg

Notes

  • The controls (Save, Next, Skip, Title, Format) do nothing. They show how each control looks.
  • Colors use theme keys, so they follow your theme. The Raster 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.
  • Built against the Claude Code mods reference (v2.1.290). Check with claude plugin validate .

Reference

Licence

MIT

Source 2 files
hooks/register.js 108 lines
1const 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}
108
hooks/client/ticker.js 16 lines
1// 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