SLOPSHOPPER

usage-pace

Franja sobre el input con el % usado de la sesión de 5 horas y de la semana, el tiempo al reset y un semáforo de ritmo

newbandtimer
★ 1v0.2.0MITupdated 2026-10-06diazgonza17/usage-pace
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · usage-pace
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM 🟢 31% 🎯 NaN% ⏳ NaNm ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
🟢 31% 🎯 NaN% ⏳ NaNm
README

usage-pace

Un mod para Claude Code que muestra, arriba del input, cuánto llevás usado de la sesión de 5 horas y de la semana, cuánto deberías llevar a esta altura y cuánto falta para el reset. Isaac te avisa si estás quemando tokens.

usage-pace en el desktop de Claude Code

Qué muestra

ElementoQué es
IsaacReacciona al ritmo de la sesión de 5 horas: pulgar arriba (verde), tranquilo (amarillo), gritando (rojo), tirado en el piso (100% usado)
Anillo + 34%Usado de la sesión de 5 horas. El anillo se llena con el % y toma el color del semáforo: verde si vas igual o por debajo de lo esperado, amarillo hasta yellowMargin puntos por encima, rojo más allá. Durante los primeros graceMinutes nunca marca rojo
Diana + 20%Lo que correspondería haber usado según el tiempo transcurrido
Reloj + 2h 41mTiempo hasta el reset
7d + anillo + 48%Usado de la semana, con el mismo semáforo. El tooltip dice cuándo se reinicia la semana y, si vas por encima, cuánto aflojar
7d + diana + 55%Lo que correspondería haber usado de la semana, repartido parejo en los 7 días (24/7)
Flecha circularLa ventana se reinició; se actualiza con la próxima respuesta

Al pasar el mouse sobre un valor aparece al instante un tooltip con su descripción. Cuando vas por encima del ritmo, el tooltip del anillo dice cuánto tiempo aflojar para volver a verde. La terminal muestra la misma información en una línea con emojis, sin tooltips:

🟢 34% 🎯 20% ⏳ 2h 41m · 7d 🟢 48% 🎯 55%

Cuándo se actualiza: el % usado llega con las respuestas de la API en la sesión (Claude Code avisa al terminar cada turno o cuando sube un punto entero). El tiempo restante y el % esperado se recalculan cada 60 segundos.

Se oculta si usás una API key en lugar de una suscripción, o si todavía no hubo ninguna respuesta en la sesión.

Instalación

El mod se instala desde el marketplace gontzalo. En Claude Code:

/plugin marketplace add diazgonza17/claude-plugins
/plugin install usage-pace@gontzalo

O desde la terminal:

claude plugin marketplace add diazgonza17/claude-plugins
claude plugin install usage-pace@gontzalo

También podés pedírselo a Claude: "Instalá el mod usage-pace de github.com/diazgonza17/usage-pace". Claude sigue las instrucciones de la sección siguiente y lo deja configurado en tu idioma.

El widget aparece arriba del input en la próxima sesión, después de la primera respuesta.

Instrucciones para Claude

Si sos Claude y te pidieron instalar este mod:

  1. Elegí el idioma del mod (es o en) según el idioma en el que el usuario te escribe en esta conversación. No uses el idioma del sistema. Si no está claro, preguntale.
  2. Agregá el marketplace e instalá el plugin pasando el idioma: ``bash claude plugin marketplace add diazgonza17/claude-plugins claude plugin install usage-pace@gontzalo --config language=<es|en> ``
  3. Decile que el widget aparece arriba del input en la próxima sesión, después de la primera respuesta, y que el idioma y los umbrales se cambian con /plugin configure usage-pace@gontzalo.

Configuración

Con /plugin configure usage-pace@gontzalo en Claude Code, o pidiéndoselo a Claude:

OpciónPor defectoQué hace
languageesIdioma de los tooltips: es o en
yellowMargin10Puntos por encima de lo esperado que se toleran en amarillo antes de pasar a rojo
showWeeklytrueMuestra el grupo 7d con el ritmo semanal. En false queda solo la sesión de 5 horas
graceMinutes15Minutos al inicio de la sesión de 5 horas en los que nunca se marca rojo
weeklyGraceHours8Horas al inicio de la semana en las que nunca se marca rojo. Por defecto es la misma proporción que los 15 minutos de la sesión

Actualizar

/plugin marketplace update gontzalo

Desarrollo

claude plugin validate .
claude plugin test .
python3 scripts/build-preview.py
python3 scripts/build-demo.py
python3 scripts/build-stage.py
python3 scripts/build-social.py
  • validate revisa el mod y lista todo lo que llama.
  • test corre los tests de la lógica, los textos y el dibujo.
  • build-preview.py dibuja con el código del plugin cada estado de ejemplo y regenera docs/preview.html, con la versión desktop en los dos idiomas (con tooltips al pasar el mouse) y la de terminal, y la captura docs/screenshot.png si tenés Chrome instalado.
  • build-demo.py arma docs/demo.gif: los cuatro estados con sus tooltips (los de 5 horas y los semanales), en inglés, para compartir. Necesita Chrome y Pillow.
  • build-stage.py genera docs/stage.html, una página para grabar la pantalla: alterna los cuatro estados cada 2 segundos sobre una réplica del input de Claude Code, con los tooltips reales al pasar el mouse. Espacio pausa, ← → cambian de estado, L cambia el idioma.
  • build-social.py genera docs/social-preview.png (1280×640), la imagen que muestra GitHub al compartir el link del repo. Se sube en Settings → Social preview.

Regenerá estos archivos cuando cambies el dibujo o los textos.

Publicar una versión: subí version en .claude-plugin/plugin.json y hacé push a main. A los usuarios solo les llega cuando cambia la versión, así que los commits intermedios no se publican.

Los textos de cada idioma están en hooks/i18n.ts.

Los sprites de Isaac están en assets/isaac/. El módulo no lee archivos y el desktop descarta imágenes embebidas, así que hooks/sprites.ts los guarda convertidos a vectores. Si cambiás un PNG, regeneralo con python3 scripts/build-sprites.py (necesita Pillow).

Licencia

MIT

Source 6 files
hooks/register.tsx 91 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionRateLimit } from 'claude-code'
3
4import type { Reading } from '../types'
5import { toLanguage } from './i18n'
6import { WEEK_MS, computePace } from './pace'
7import { PILL_HEIGHT, itemsFor, pillSvg, textLabel } from './pill'
8
9const fiveHour = atom({ plugin: 'usage-pace', key: 'fiveHour' } as const, null)
10const sevenDay = atom({ plugin: 'usage-pace', key: 'sevenDay' } as const, null)
11
12const toReading = (limits: readonly SessionRateLimit[], kind: string): Reading | null => {
13  const window = limits.find(limit => limit.kind === kind)
14  if (!window?.resetsAt) {
15    return null
16  }
17
18  return { percentUsed: window.percentUsed, resetsAt: window.resetsAt }
19}
20
21const saveReadings = async ($: EngineInterface, limits: readonly SessionRateLimit[]) => {
22  const reading = toReading(limits, 'five_hour')
23  const weekReading = toReading(limits, 'seven_day')
24  await update($, fiveHour, () => reading)
25  await update($, sevenDay, () => weekReading)
26}
27
28export const register: Register = (on, options) => {
29  const config = {
30    yellowMargin: Number(options.yellowMargin ?? 10),
31    graceMinutes: Number(options.graceMinutes ?? 15),
32  }
33  const weekConfig = { ...config, graceMinutes: Number(options.weeklyGraceHours ?? 8) * 60 }
34  const showWeekly = Boolean(options.showWeekly ?? true)
35  const language = toLanguage(options.language)
36
37  on('session.start', async ($, e, next) => {
38    const { rateLimits } = await $.session.usage()
39    if (rateLimits.length > 0) {
40      await saveReadings($, rateLimits)
41    }
42    // Keeps the countdown and the expected % fresh between measurements.
43    $.clock.every(60_000, () => $.ui.invalidate('ui.render'))
44
45    return next(e)
46  })
47
48  on('session.measure', async ($, e, next) => {
49    if (e.changed.includes('rateLimits')) {
50      await saveReadings($, e.rateLimits)
51    }
52
53    return next(e)
54  })
55
56  // One row above the prompt; on desktop an SVG so each value carries a native tooltip.
57  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
58    const reading = await read($, fiveHour)
59    const weekReading = showWeekly ? await read($, sevenDay) : null
60    if (e.props.hasSurvey || (reading === null && weekReading === null)) {
61      return next(e)
62    }
63
64    const now = await $.clock.now()
65    const pace = reading && computePace(reading.percentUsed, Date.parse(reading.resetsAt), now, config)
66    const weekPace =
67      weekReading && computePace(weekReading.percentUsed, Date.parse(weekReading.resetsAt), now, weekConfig, WEEK_MS)
68    const items = itemsFor(pace, language, weekPace)
69
70    if (e.surface === 'desktop') {
71      const { Box, Svg } = $.ui.resolve(e)
72      const { source, width } = pillSvg(items)
73      const alt = items.map(item => item.tip).join('. ')
74
75      return (
76        <Box flexDirection="row">
77          <Svg source={source} alt={alt} width={width} height={PILL_HEIGHT} isInteractive />
78        </Box>
79      )
80    }
81
82    const { Box, Text } = $.ui.resolve(e)
83
84    return (
85      <Box flexDirection="row">
86        <Text dimColor>{textLabel(items)}</Text>
87      </Box>
88    )
89  })
90}
91
hooks/i18n.ts 48 lines
1export type Language = 'es' | 'en'
2
3export type Strings = {
4  used: string
5  doingGood: string
6  lookOut: (wait: string) => string
7  burning: (wait: string) => string
8  limit: string
9  expected: string
10  reset: string
11  stale: string
12  weekUsed: (reset: string) => string
13  weekExpected: string
14  weekStale: string
15}
16
17export const STRINGS: Record<Language, Strings> = {
18  es: {
19    used: 'Usado de la sesión de 5 horas',
20    doingGood: 'venís bien',
21    lookOut: wait => `cuidado (aflojá por ${wait})`,
22    burning: wait => `estás quemando tokens! (pisá el freno por ${wait})`,
23    limit: 'límite alcanzado',
24    expected: 'Lo que correspondería haber usado a esta altura',
25    reset: 'Tiempo hasta que se reinicie la sesión',
26    stale: 'La sesión se reinició; se actualiza con la próxima respuesta',
27    weekUsed: reset => `Usado de la semana, se reinicia en ${reset}`,
28    weekExpected: 'Lo que correspondería haber usado esta semana',
29    weekStale: 'La semana se reinició; se actualiza con la próxima respuesta',
30  },
31  en: {
32    used: 'Used of the 5-hour session',
33    doingGood: 'doing good',
34    lookOut: wait => `look out (slow down for ${wait})`,
35    burning: wait => `you're burning tokens! (press the brakes for ${wait})`,
36    limit: 'limit reached',
37    expected: 'What you should have used by now',
38    reset: 'Time until the session resets',
39    stale: 'The session reset; it updates with the next response',
40    weekUsed: reset => `Used this week, resets in ${reset}`,
41    weekExpected: 'What you should have used this week by now',
42    weekStale: 'The week reset; it updates with the next response',
43  },
44}
45
46// The `language` option, Spanish when unset or unknown.
47export const toLanguage = (value: unknown): Language => (value === 'en' ? 'en' : 'es')
48
hooks/pace.ts 75 lines
1export const WINDOW_MS = 5 * 60 * 60 * 1000
2export const WEEK_MS = 7 * 24 * 60 * 60 * 1000
3
4export type Light = 'green' | 'yellow' | 'red'
5
6export type Pace =
7  | { kind: 'stale' }
8  | {
9      kind: 'live'
10      light: Light
11      used: number
12      expected: number
13      remainingMs: number
14      /** How long without using Claude until the pace is green again. */
15      slowDownMs: number
16    }
17
18export type PaceConfig = { yellowMargin: number; graceMinutes: number }
19
20// Expected usage grows linearly over the window: with 4h left of 5h, ~20% is on pace.
21export const computePace = (
22  used: number,
23  resetsAtMs: number,
24  nowMs: number,
25  { yellowMargin, graceMinutes }: PaceConfig,
26  windowMs = WINDOW_MS,
27): Pace => {
28  const remainingMs = resetsAtMs - nowMs
29  if (remainingMs <= 0) {
30    return { kind: 'stale' }
31  }
32
33  const elapsedMs = Math.max(0, windowMs - remainingMs)
34  const expected = (elapsedMs / windowMs) * 100
35  const delta = used - expected
36  const isInGrace = elapsedMs < graceMinutes * 60 * 1000
37
38  let light: Light = 'green'
39  if (used >= 100) {
40    light = 'red'
41  } else if (delta > yellowMargin && !isInGrace) {
42    light = 'red'
43  } else if (delta > 0) {
44    light = 'yellow'
45  }
46
47  // Paused, `used` stays put while `expected` keeps growing: the gap closes at
48  // 100 points per window.
49  const slowDownMs = Math.max(0, (delta / 100) * windowMs)
50
51  return { kind: 'live', light, used, expected, remainingMs, slowDownMs }
52}
53
54export const formatRemaining = (ms: number): string => {
55  const minutes = Math.ceil(ms / 60000)
56  const days = Math.floor(minutes / (24 * 60))
57  const hours = Math.floor(minutes / 60) % 24
58  const rest = minutes % 60
59  if (days > 0) return `${days}d ${hours}h`
60
61  return hours > 0 ? `${hours}h ${String(rest).padStart(2, '0')}m` : `${rest}m`
62}
63
64// "15 min", "1h", "1h 15min", "1d 3h": the wait a tip suggests.
65export const formatWait = (ms: number): string => {
66  const minutes = Math.max(1, Math.ceil(ms / 60000))
67  const days = Math.floor(minutes / (24 * 60))
68  const hours = Math.floor(minutes / 60) % 24
69  const rest = minutes % 60
70  if (days > 0) return hours === 0 ? `${days}d` : `${days}d ${hours}h`
71  if (hours === 0) return `${rest} min`
72
73  return rest === 0 ? `${hours}h` : `${hours}h ${rest}min`
74}
75
hooks/pill.ts 235 lines
1import { STRINGS } from './i18n'
2import type { Language, Strings } from './i18n'
3import { formatRemaining, formatWait } from './pace'
4import type { Light, Pace } from './pace'
5import { SPRITES } from './sprites'
6
7export type Icon = 'pace' | 'target' | 'clock' | 'refresh'
8
9// `prefix` names the window a group belongs to ("7d"); the 5-hour group has
10// Isaac instead.
11export type Item = { icon: Icon; label: string; tip: string; light?: Light; used?: number; prefix?: string }
12
13// The second half of the progress tip; off pace, how long to ease off to be
14// green again (always before the reset while under 100%).
15const rhythm = (pace: Extract<Pace, { kind: 'live' }>, text: Strings): string => {
16  if (pace.used >= 100) return text.limit
17  if (pace.light === 'green') return text.doingGood
18
19  const wait = formatWait(pace.slowDownMs)
20
21  return pace.light === 'yellow' ? text.lookOut(wait) : text.burning(wait)
22}
23
24const WEEK_PREFIX = '7d'
25
26// The 5-hour group (Isaac, ring, expected, reset) and, when there is a weekly
27// reading, the weekly one (ring and expected; its reset is in the ring's tip).
28export const itemsFor = (pace: Pace | null, language: Language, weekPace: Pace | null = null): Item[] => {
29  const text = STRINGS[language]
30  const items: Item[] = []
31
32  if (pace?.kind === 'stale') {
33    items.push({ icon: 'refresh', label: '', tip: text.stale })
34  } else if (pace) {
35    items.push(
36      {
37        icon: 'pace',
38        label: `${Math.round(pace.used)}%`,
39        tip: `${text.used} · ${rhythm(pace, text)}`,
40        light: pace.light,
41        used: pace.used,
42      },
43      { icon: 'target', label: `${Math.round(pace.expected)}%`, tip: text.expected },
44      { icon: 'clock', label: formatRemaining(pace.remainingMs), tip: text.reset },
45    )
46  }
47
48  if (weekPace?.kind === 'stale') {
49    items.push({ icon: 'refresh', label: '', tip: text.weekStale, prefix: WEEK_PREFIX })
50  } else if (weekPace) {
51    items.push(
52      {
53        icon: 'pace',
54        label: `${Math.round(weekPace.used)}%`,
55        tip: `${text.weekUsed(formatRemaining(weekPace.remainingMs))} · ${rhythm(weekPace, text)}`,
56        light: weekPace.light,
57        used: weekPace.used,
58        prefix: WEEK_PREFIX,
59      },
60      { icon: 'target', label: `${Math.round(weekPace.expected)}%`, tip: text.weekExpected },
61    )
62  }
63
64  return items
65}
66
67const EMOJI: Record<Light, string> = { green: '🟢', yellow: '🟡', red: '🔴' }
68
69// The terminal draws text only: each icon as its emoji, the weekly group after a dot.
70export const textLabel = (items: Item[]): string =>
71  items
72    .map((item, index) => {
73      const emoji = item.icon === 'pace' ? EMOJI[item.light ?? 'green'] : { target: '🎯', clock: '⏳', refresh: '🔄' }[item.icon]
74      const value = item.label === '' ? emoji : `${emoji} ${item.label}`
75      if (!item.prefix) return value
76
77      return `${index > 0 ? '· ' : ''}${item.prefix} ${value}`
78    })
79    .join(' ')
80
81const FILL: Record<Light, string> = { green: '#22c55e', yellow: '#eab308', red: '#ef4444' }
82
83const HEIGHT = 20
84const MID = HEIGHT / 2
85const ICON = 14
86const ICON_GAP = 5
87const ITEM_GAP = 14
88const LABEL_SIZE = 13
89const TIP_SIZE = 12
90const TIP_PAD = 8
91const FONT = 'system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif'
92
93// Advance widths of the macOS system font at 12px, measured in the desktop's
94// engine; other sizes scale. Labels and tips also get `textLength`, so a
95// slightly different font keeps the layout exact.
96const WIDTHS_12: Record<string, number> = {
97  ...Object.fromEntries('0123456789'.split('').map((d, i) => [d, [7.56, 5.57, 7.24, 7.52, 7.73, 7.42, 7.64, 6.84, 7.66, 7.64][i]])),
98  a: 6.63, b: 7.38, c: 6.72, d: 7.38, e: 6.86, f: 4.34, g: 7.31, h: 7.06, i: 2.97, j: 2.96, k: 6.52, l: 3.04,
99  m: 10.45, n: 7.01, o: 7.09, p: 7.33, q: 7.31, r: 4.57, s: 6.28, t: 4.36, u: 7.01, v: 6.51, w: 9.3, x: 6.3,
100  y: 6.52, z: 6.47, A: 8.09, L: 6.82, T: 7.61, U: 8.85, W: 11.62, "'": 2.6, '-': 5.66, '!': 3.73, ' ': 3.2, '.': 3.56, ',': 3.56, ';': 3.56, ':': 3.56,
101  '%': 11.11, '·': 3.56, '(': 4.59, ')': 4.59, '+': 7.56, á: 6.63, é: 6.86, í: 2.97, ó: 7.09, ú: 7.01, ñ: 7.01,
102}
103
104const textWidth = (text: string, size: number) =>
105  Array.from(text).reduce((sum, char) => sum + (WIDTHS_12[char] ?? 7), 0) * (size / 12)
106
107const escape = (text: string) =>
108  text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
109
110const iconSvg = (item: Item, x: number): string => {
111  const cx = x + ICON / 2
112  const r = 5.5
113  switch (item.icon) {
114    case 'pace': {
115      const circumference = 2 * Math.PI * r
116      const filled = Math.max(1, (Math.min(100, item.used ?? 0) / 100) * circumference)
117      return (
118        `<circle class="track" cx="${cx}" cy="${MID}" r="${r}" fill="none" stroke-width="2.4"/>` +
119        `<circle cx="${cx}" cy="${MID}" r="${r}" fill="none" stroke="${FILL[item.light ?? 'green']}" stroke-width="2.4" ` +
120        `stroke-linecap="round" stroke-dasharray="${filled.toFixed(2)} ${circumference.toFixed(2)}" transform="rotate(-90 ${cx} ${MID})"/>`
121      )
122    }
123    case 'target':
124      // A target with an arrow stuck in its centre, fletching up and to the right.
125      return (
126        `<circle class="line" cx="${cx}" cy="${MID}" r="${r}" fill="none" stroke-width="1.3"/>` +
127        `<circle class="line" cx="${cx}" cy="${MID}" r="2.5" fill="none" stroke-width="1.3"/>` +
128        `<path class="line" d="M${cx} ${MID}L${cx + 6.2} ${MID - 6.2}" fill="none" stroke-width="1.4" stroke-linecap="round"/>` +
129        `<path class="line" d="M${cx + 4.4} ${MID - 7}l1.8 0.8l0.8 1.8M${cx + 3.2} ${MID - 5.8}l1.8 0.8l0.8 1.8" fill="none" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>` +
130        `<circle class="dot" cx="${cx}" cy="${MID}" r="1.1"/>`
131      )
132    case 'clock':
133      return (
134        `<circle class="line" cx="${cx}" cy="${MID}" r="${r}" fill="none" stroke-width="1.3"/>` +
135        `<path class="line" d="M${cx} ${MID - 3.2}V${MID}l2.3 1.4" fill="none" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>`
136      )
137    case 'refresh':
138      return (
139        `<path class="line" d="M${cx + 4.6} ${MID - 2.4}A5 5 0 1 0 ${cx + 5} ${MID + 0.6}" fill="none" stroke-width="1.3" stroke-linecap="round"/>` +
140        `<path class="line" d="M${cx + 4.9} ${MID - 5.2}V${MID - 2.2}H${cx + 1.9}" fill="none" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>`
141      )
142  }
143}
144
145const ISAAC_GAP = 5
146const PREFIX_GAP = 4
147const SEPARATOR_GAP = 8
148
149// Isaac reacts to the pace: thumbs up, idle, screaming, or flat on the floor at
150// the limit. Drawn as vector pixels (the desktop strips embedded images) at half
151// their native size, so a 2x screen shows each sprite pixel crisp.
152const isaacSvg = (item: Item, x: number): { markup: string; width: number } => {
153  const sprite = (item.used ?? 0) >= 100 ? SPRITES.limit : SPRITES[item.light ?? 'green']
154  const width = sprite.width / 2
155  const y = MID - sprite.height / 4
156  const paths = sprite.paths.map(([fill, d]) => `<path fill="${fill}" d="${d}"/>`).join('')
157  const markup = `<g transform="translate(${x} ${y}) scale(0.5)" shape-rendering="crispEdges">${paths}</g>`
158
159  return { markup, width }
160}
161
162const STYLE = [
163  ':root{color-scheme:light dark;background:transparent}',
164  `text{font-family:${FONT}}`,
165  '.label{font-size:13px;fill:#6b6b6b}.prefix{font-weight:600}.line{stroke:#6b6b6b}.dot{fill:#6b6b6b}.track{stroke:#d9d9d9}',
166  '.item{cursor:default}.item:hover .label{fill:#1a1a1a}.item:hover .line{stroke:#1a1a1a}.item:hover .dot{fill:#1a1a1a}',
167  '.tip{opacity:0;pointer-events:none;transition:opacity 80ms}',
168  '.tip rect,.tip path{fill:#fff;stroke:#d4d4d4}.tip text{font-size:12px;fill:#1a1a1a}',
169  '@media (prefers-color-scheme:dark){',
170  '.label{fill:#9b9b9b}.line{stroke:#9b9b9b}.dot{fill:#9b9b9b}.track{stroke:#3d3d3d}',
171  '.item:hover .label{fill:#ececec}.item:hover .line{stroke:#ececec}.item:hover .dot{fill:#ececec}',
172  '.tip rect,.tip path{fill:#2b2b2b;stroke:#454545}.tip text{fill:#ececec}}',
173].join('')
174
175// One SVG for the desktop. Each value is a hover group; its tip is a sibling
176// shown by CSS the moment the pointer is on it, to the right of the values
177// (the frame is one row tall, so nothing can float above it).
178export const pillSvg = (items: Item[]): { source: string; width: number } => {
179  let x = 1
180  const groups: string[] = []
181  for (const [index, item] of items.entries()) {
182    // A thin rule between the 5-hour group and the weekly one.
183    if (item.prefix && index > 0) {
184      const ruleX = x - ITEM_GAP / 2 + SEPARATOR_GAP / 2
185      groups.push(`<path class="track" d="M${ruleX} 4V${HEIGHT - 4}" stroke-width="1"/>`)
186      x += SEPARATOR_GAP
187    }
188    const start = x
189    const isaac = item.icon === 'pace' && !item.prefix ? isaacSvg(item, x) : null
190    if (isaac) {
191      x += isaac.width + ISAAC_GAP
192    }
193    let prefix = ''
194    if (item.prefix) {
195      const prefixWidth = textWidth(item.prefix, LABEL_SIZE)
196      prefix = `<text class="label prefix" x="${x}" y="${MID}" dominant-baseline="central" textLength="${prefixWidth.toFixed(2)}" lengthAdjust="spacing">${escape(item.prefix)}</text>`
197      x += prefixWidth + PREFIX_GAP
198    }
199    const labelWidth = item.label === '' ? 0 : textWidth(item.label, LABEL_SIZE)
200    const label =
201      labelWidth > 0
202        ? `<text class="label" x="${x + ICON + ICON_GAP}" y="${MID}" dominant-baseline="central" textLength="${labelWidth.toFixed(2)}" lengthAdjust="spacing">${escape(item.label)}</text>`
203        : ''
204    const end = x + ICON + (labelWidth > 0 ? ICON_GAP + labelWidth : 0)
205    groups.push(
206      `<g class="item" id="i${index}"><rect x="${start - 3}" y="0" width="${end - start + 6}" height="${HEIGHT}" fill="transparent"/>${isaac?.markup ?? ''}${prefix}${iconSvg(item, x)}${label}</g>`,
207    )
208    x = end + ITEM_GAP
209  }
210
211  const tipX = x - ITEM_GAP + 10
212  let widest = 0
213  const tips = items.map((item, index) => {
214    const textW = textWidth(item.tip, TIP_SIZE)
215    const boxW = textW + TIP_PAD * 2
216    widest = Math.max(widest, boxW)
217    const caret = `<path d="M${tipX + 0.5} ${MID - 4}L${tipX - 4} ${MID}L${tipX + 0.5} ${MID + 4}" stroke-width="1"/>`
218    return (
219      `<g class="tip" id="t${index}">` +
220      `<rect x="${tipX}" y="1" width="${boxW.toFixed(2)}" height="${HEIGHT - 2}" rx="5" stroke-width="1"/>${caret}` +
221      `<text x="${tipX + TIP_PAD}" y="${MID}" dominant-baseline="central" textLength="${textW.toFixed(2)}" lengthAdjust="spacing">${escape(item.tip)}</text>` +
222      `</g>`
223    )
224  })
225
226  const reveal = items.map((_, index) => `#i${index}:hover~#t${index}`).join(',')
227  const width = Math.ceil(tipX + widest + 2)
228  const style = `${STYLE}${reveal}{opacity:1}`
229  const source = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${HEIGHT}" viewBox="0 0 ${width} ${HEIGHT}"><style>${style}</style>${groups.join('')}${tips.join('')}</svg>`
230
231  return { source, width }
232}
233
234export const PILL_HEIGHT = HEIGHT
235
hooks/sprites.ts 11 lines
1// Generated by scripts/build-sprites.py from assets/isaac/*.png. Do not edit.
2
3export type Sprite = { width: number; height: number; paths: readonly (readonly [fill: string, d: string])[] }
4
5export const SPRITES = {
6  green: { width: 34, height: 33, paths: [['#080000', 'M10 0h8v1h-8zM8 1h12v1h-12zM6 2h4v1h-4zM18 2h4v1h-4zM5 3h3v1h-3zM20 3h3v1h-3zM4 4h2v1h-2zM22 4h2v1h-2zM3 5h2v1h-2zM23 5h6v1h-6zM2 6h2v1h-2zM24 6h6v1h-6zM2 7h2v1h-2zM24 7h2v1h-2zM29 7h2v1h-2zM1 8h2v1h-2zM24 8h2v1h-2zM29 8h2v1h-2zM1 9h2v1h-2zM24 9h1v1h-1zM29 9h2v1h-2zM0 10h2v1h-2zM24 10h1v1h-1zM29 10h2v1h-2zM0 11h2v1h-2zM5 11h4v1h-4zM19 11h4v1h-4zM24 11h1v1h-1zM29 11h2v1h-2zM0 12h2v1h-2zM4 12h1v1h-1zM7 12h3v1h-3zM18 12h1v1h-1zM21 12h4v1h-4zM27 12h5v1h-5zM0 13h2v1h-2zM4 13h1v1h-1zM7 13h3v1h-3zM18 13h1v1h-1zM21 13h3v1h-3zM26 13h7v1h-7zM0 14h2v1h-2zM4 14h6v1h-6zM11 14h6v1h-6zM18 14h6v1h-6zM26 14h1v1h-1zM31 14h2v1h-2zM1 15h2v1h-2zM11 15h1v1h-1zM16 15h1v1h-1zM22 15h2v1h-2zM25 15h1v1h-1zM32 15h2v1h-2zM1 16h2v1h-2zM11 16h6v1h-6zM21 16h2v1h-2zM25 16h1v1h-1zM32 16h2v1h-2zM2 17h2v1h-2zM12 17h4v1h-4zM20 17h3v1h-3zM32 17h2v1h-2zM2 18h2v1h-2zM20 18h1v1h-1zM22 18h1v1h-1zM32 18h2v1h-2zM3 19h2v1h-2zM19 19h2v1h-2zM32 19h2v1h-2zM4 20h2v1h-2zM19 20h1v1h-1zM32 20h2v1h-2zM5 21h3v1h-3zM19 21h1v1h-1zM23 21h1v1h-1zM31 21h3v1h-3zM6 22h4v1h-4zM18 22h2v1h-2zM24 22h2v1h-2zM30 22h3v1h-3zM7 23h13v1h-13zM25 23h7v1h-7zM7 24h2v1h-2zM10 24h11v1h-11zM24 24h7v1h-7zM6 25h2v1h-2zM19 25h7v1h-7zM6 26h2v1h-2zM10 26h1v1h-1zM19 26h6v1h-6zM6 27h2v1h-2zM10 27h1v1h-1zM19 27h3v1h-3zM7 28h4v1h-4zM14 28h2v1h-2zM19 28h2v1h-2zM8 29h3v1h-3zM14 29h2v1h-2zM19 29h2v1h-2zM9 30h3v1h-3zM14 30h2v1h-2zM18 30h3v1h-3zM10 31h10v1h-10zM11 32h3v1h-3zM16 32h3v1h-3z'], ['#cf9c9b', 'M10 2h8v1h-8zM8 3h1v1h-1zM19 3h1v1h-1zM6 4h1v1h-1zM21 4h1v1h-1zM5 5h1v1h-1zM22 5h1v1h-1zM4 6h1v1h-1zM23 6h1v1h-1zM26 7h1v1h-1zM28 7h1v1h-1zM3 8h1v1h-1zM25 9h1v1h-1zM2 10h1v1h-1zM25 10h1v1h-1zM2 11h1v1h-1zM4 11h1v1h-1zM9 11h1v1h-1zM18 11h1v1h-1zM23 11h1v1h-1zM25 11h1v1h-1zM28 11h1v1h-1zM2 12h1v1h-1zM25 12h2v1h-2zM2 13h2v1h-2zM25 13h1v1h-1zM3 14h1v1h-1zM24 14h2v1h-2zM27 14h1v1h-1zM30 14h1v1h-1zM3 15h1v1h-1zM5 15h4v1h-4zM19 15h3v1h-3zM24 15h1v1h-1zM26 15h1v1h-1zM31 15h1v1h-1zM4 16h1v1h-1zM23 16h2v1h-2zM4 17h1v1h-1zM11 17h1v1h-1zM16 17h1v1h-1zM23 17h2v1h-2zM5 18h1v1h-1zM23 18h3v1h-3zM6 19h1v1h-1zM23 19h4v1h-4zM7 20h1v1h-1zM20 20h1v1h-1zM23 20h4v1h-4zM9 21h1v1h-1zM18 21h1v1h-1zM20 21h2v1h-2zM25 21h5v1h-5zM11 22h6v1h-6zM20 22h2v1h-2zM23 22h1v1h-1zM27 22h2v1h-2zM22 23h2v1h-2zM9 24h1v1h-1zM8 25h1v1h-1zM11 25h8v1h-8zM8 26h1v1h-1zM11 26h1v1h-1zM18 26h1v1h-1zM8 27h1v1h-1zM13 28h1v1h-1zM16 28h1v1h-1zM16 29h1v1h-1zM16 30h1v1h-1z'], ['#e3c6c5', 'M9 3h10v1h-10zM7 4h14v1h-14zM6 5h16v1h-16zM5 6h18v1h-18zM4 7h20v1h-20zM27 7h1v1h-1zM4 8h20v1h-20zM26 8h3v1h-3zM3 9h21v1h-21zM26 9h3v1h-3zM3 10h21v1h-21zM26 10h3v1h-3zM3 11h1v1h-1zM10 11h8v1h-8zM26 11h2v1h-2zM3 12h1v1h-1zM10 12h8v1h-8zM10 13h8v1h-8zM10 14h1v1h-1zM17 14h1v1h-1zM28 14h2v1h-2zM10 15h1v1h-1zM17 15h1v1h-1zM27 15h4v1h-4zM5 16h6v1h-6zM17 16h4v1h-4zM26 16h6v1h-6zM5 17h6v1h-6zM17 17h3v1h-3zM26 17h6v1h-6zM6 18h14v1h-14zM26 18h6v1h-6zM7 19h12v1h-12zM21 19h2v1h-2zM27 19h5v1h-5zM8 20h11v1h-11zM21 20h2v1h-2zM27 20h5v1h-5zM10 21h8v1h-8zM22 21h1v1h-1zM22 22h1v1h-1zM9 25h2v1h-2zM9 26h1v1h-1zM12 26h6v1h-6zM9 27h1v1h-1zM11 27h8v1h-8zM11 28h2v1h-2zM17 28h2v1h-2zM11 29h3v1h-3zM17 29h2v1h-2zM12 30h2v1h-2zM17 30h1v1h-1z'], ['#ffffff', 'M5 12h2v1h-2zM19 12h2v1h-2zM5 13h2v1h-2zM19 13h2v1h-2zM12 15h4v1h-4z'], ['#b97371', 'M24 13h1v1h-1zM2 14h1v1h-1zM4 15h1v1h-1zM9 15h1v1h-1zM18 15h1v1h-1zM3 16h1v1h-1zM25 17h1v1h-1zM4 18h1v1h-1zM21 18h1v1h-1zM5 19h1v1h-1zM6 20h1v1h-1zM8 21h1v1h-1zM24 21h1v1h-1zM30 21h1v1h-1zM10 22h1v1h-1zM17 22h1v1h-1zM26 22h1v1h-1zM29 22h1v1h-1zM20 23h2v1h-2zM24 23h1v1h-1zM21 24h3v1h-3z']] },
7  yellow: { width: 28, height: 33, paths: [['#080000', 'M10 0h8v1h-8zM8 1h12v1h-12zM6 2h4v1h-4zM18 2h4v1h-4zM5 3h3v1h-3zM20 3h3v1h-3zM4 4h2v1h-2zM22 4h2v1h-2zM3 5h2v1h-2zM23 5h2v1h-2zM2 6h2v1h-2zM24 6h2v1h-2zM2 7h2v1h-2zM24 7h2v1h-2zM1 8h2v1h-2zM25 8h2v1h-2zM1 9h2v1h-2zM25 9h2v1h-2zM0 10h2v1h-2zM26 10h2v1h-2zM0 11h2v1h-2zM5 11h4v1h-4zM19 11h4v1h-4zM26 11h2v1h-2zM0 12h2v1h-2zM4 12h1v1h-1zM7 12h3v1h-3zM18 12h1v1h-1zM21 12h3v1h-3zM26 12h2v1h-2zM0 13h2v1h-2zM4 13h1v1h-1zM7 13h3v1h-3zM18 13h1v1h-1zM21 13h3v1h-3zM26 13h2v1h-2zM0 14h2v1h-2zM4 14h6v1h-6zM12 14h4v1h-4zM18 14h6v1h-6zM26 14h2v1h-2zM1 15h2v1h-2zM4 15h6v1h-6zM11 15h6v1h-6zM18 15h6v1h-6zM25 15h2v1h-2zM1 16h2v1h-2zM5 16h4v1h-4zM11 16h1v1h-1zM16 16h1v1h-1zM19 16h4v1h-4zM25 16h2v1h-2zM2 17h2v1h-2zM11 17h6v1h-6zM24 17h2v1h-2zM2 18h2v1h-2zM24 18h2v1h-2zM3 19h2v1h-2zM23 19h2v1h-2zM4 20h2v1h-2zM22 20h2v1h-2zM5 21h3v1h-3zM20 21h3v1h-3zM6 22h4v1h-4zM18 22h4v1h-4zM7 23h14v1h-14zM6 24h2v1h-2zM10 24h8v1h-8zM20 24h2v1h-2zM5 25h2v1h-2zM21 25h2v1h-2zM5 26h2v1h-2zM9 26h1v1h-1zM18 26h1v1h-1zM21 26h2v1h-2zM5 27h2v1h-2zM9 27h1v1h-1zM18 27h1v1h-1zM21 27h2v1h-2zM6 28h4v1h-4zM13 28h2v1h-2zM18 28h4v1h-4zM7 29h3v1h-3zM13 29h2v1h-2zM18 29h3v1h-3zM8 30h3v1h-3zM13 30h2v1h-2zM17 30h3v1h-3zM9 31h10v1h-10zM10 32h3v1h-3zM15 32h3v1h-3z'], ['#cf9c9b', 'M10 2h8v1h-8zM8 3h1v1h-1zM19 3h1v1h-1zM6 4h1v1h-1zM21 4h1v1h-1zM5 5h1v1h-1zM22 5h1v1h-1zM4 6h1v1h-1zM23 6h1v1h-1zM3 8h1v1h-1zM24 8h1v1h-1zM2 10h1v1h-1zM25 10h1v1h-1zM2 11h1v1h-1zM4 11h1v1h-1zM9 11h1v1h-1zM18 11h1v1h-1zM23 11h1v1h-1zM25 11h1v1h-1zM2 12h1v1h-1zM25 12h1v1h-1zM2 13h2v1h-2zM24 13h2v1h-2zM3 14h1v1h-1zM24 14h1v1h-1zM3 15h1v1h-1zM24 15h1v1h-1zM4 16h1v1h-1zM9 16h1v1h-1zM18 16h1v1h-1zM23 16h1v1h-1zM4 17h1v1h-1zM8 17h1v1h-1zM19 17h1v1h-1zM23 17h1v1h-1zM8 20h1v1h-1zM19 20h1v1h-1zM9 21h1v1h-1zM18 21h1v1h-1zM11 22h6v1h-6zM8 24h1v1h-1zM19 24h1v1h-1zM7 25h1v1h-1zM10 25h8v1h-8zM20 25h1v1h-1zM7 26h1v1h-1zM10 26h1v1h-1zM17 26h1v1h-1zM20 26h1v1h-1zM7 27h1v1h-1zM20 27h1v1h-1zM12 28h1v1h-1zM15 28h1v1h-1zM15 29h1v1h-1zM15 30h1v1h-1z'], ['#e3c6c5', 'M9 3h10v1h-10zM7 4h14v1h-14zM6 5h16v1h-16zM5 6h18v1h-18zM4 7h20v1h-20zM4 8h20v1h-20zM3 9h22v1h-22zM3 10h22v1h-22zM3 11h1v1h-1zM10 11h8v1h-8zM24 11h1v1h-1zM3 12h1v1h-1zM10 12h8v1h-8zM24 12h1v1h-1zM10 13h8v1h-8zM10 14h2v1h-2zM16 14h2v1h-2zM10 15h1v1h-1zM17 15h1v1h-1zM10 16h1v1h-1zM17 16h1v1h-1zM9 17h2v1h-2zM17 17h2v1h-2zM8 18h12v1h-12zM8 19h12v1h-12zM9 20h10v1h-10zM10 21h8v1h-8zM9 24h1v1h-1zM18 24h1v1h-1zM8 25h2v1h-2zM18 25h2v1h-2zM8 26h1v1h-1zM11 26h6v1h-6zM19 26h1v1h-1zM8 27h1v1h-1zM10 27h8v1h-8zM19 27h1v1h-1zM10 28h2v1h-2zM16 28h2v1h-2zM10 29h3v1h-3zM16 29h2v1h-2zM11 30h2v1h-2zM16 30h1v1h-1z'], ['#ffffff', 'M5 12h2v1h-2zM19 12h2v1h-2zM5 13h2v1h-2zM19 13h2v1h-2zM12 16h4v1h-4z'], ['#b97371', 'M2 14h1v1h-1zM25 14h1v1h-1zM3 16h1v1h-1zM24 16h1v1h-1zM4 18h1v1h-1zM23 18h1v1h-1zM10 22h1v1h-1zM17 22h1v1h-1z'], ['#9cc8cc', 'M5 17h3v1h-3zM20 17h3v1h-3zM6 19h1v1h-1zM20 19h1v1h-1zM7 20h1v1h-1zM20 20h1v1h-1z'], ['#9cc8cd', 'M5 18h3v1h-3zM20 18h3v1h-3zM21 19h1v1h-1z'], ['#68989c', 'M5 19h1v1h-1z'], ['#9dc9cd', 'M7 19h1v1h-1z'], ['#67979c', 'M22 19h1v1h-1zM6 20h1v1h-1zM19 21h1v1h-1z'], ['#67979b', 'M21 20h1v1h-1zM8 21h1v1h-1z']] },
8  red: { width: 28, height: 33, paths: [['#080000', 'M10 0h8v1h-8zM8 1h12v1h-12zM6 2h4v1h-4zM18 2h4v1h-4zM5 3h3v1h-3zM20 3h3v1h-3zM4 4h2v1h-2zM22 4h2v1h-2zM3 5h2v1h-2zM23 5h2v1h-2zM2 6h2v1h-2zM24 6h2v1h-2zM2 7h2v1h-2zM5 7h4v1h-4zM19 7h4v1h-4zM24 7h2v1h-2zM1 8h2v1h-2zM4 8h1v1h-1zM7 8h3v1h-3zM18 8h1v1h-1zM21 8h3v1h-3zM25 8h2v1h-2zM1 9h2v1h-2zM4 9h1v1h-1zM7 9h3v1h-3zM18 9h1v1h-1zM21 9h3v1h-3zM25 9h2v1h-2zM0 10h2v1h-2zM4 10h6v1h-6zM12 10h4v1h-4zM18 10h6v1h-6zM26 10h2v1h-2zM0 11h2v1h-2zM4 11h6v1h-6zM11 11h1v1h-1zM16 11h1v1h-1zM18 11h6v1h-6zM26 11h2v1h-2zM0 12h2v1h-2zM11 12h1v1h-1zM13 12h2v1h-2zM16 12h1v1h-1zM26 12h2v1h-2zM0 13h2v1h-2zM11 13h6v1h-6zM26 13h2v1h-2zM0 14h2v1h-2zM11 14h6v1h-6zM26 14h2v1h-2zM1 15h6v1h-6zM11 15h6v1h-6zM21 15h6v1h-6zM1 16h7v1h-7zM11 16h2v1h-2zM15 16h2v1h-2zM20 16h7v1h-7zM0 17h3v1h-3zM6 17h3v1h-3zM12 17h4v1h-4zM19 17h3v1h-3zM25 17h3v1h-3zM0 18h2v1h-2zM7 18h2v1h-2zM19 18h2v1h-2zM26 18h2v1h-2zM0 19h2v1h-2zM7 19h2v1h-2zM19 19h2v1h-2zM26 19h2v1h-2zM0 20h2v1h-2zM7 20h2v1h-2zM19 20h2v1h-2zM26 20h2v1h-2zM0 21h2v1h-2zM7 21h2v1h-2zM19 21h2v1h-2zM26 21h2v1h-2zM1 22h2v1h-2zM8 22h2v1h-2zM18 22h2v1h-2zM25 22h2v1h-2zM2 23h2v1h-2zM8 23h12v1h-12zM24 23h2v1h-2zM3 24h3v1h-3zM8 24h12v1h-12zM22 24h3v1h-3zM4 25h6v1h-6zM18 25h6v1h-6zM6 26h4v1h-4zM18 26h4v1h-4zM8 27h2v1h-2zM18 27h2v1h-2zM8 28h2v1h-2zM13 28h2v1h-2zM18 28h2v1h-2zM8 29h2v1h-2zM13 29h2v1h-2zM18 29h2v1h-2zM8 30h3v1h-3zM13 30h2v1h-2zM17 30h3v1h-3zM9 31h10v1h-10zM10 32h3v1h-3zM15 32h3v1h-3z'], ['#cf9c9b', 'M10 2h8v1h-8zM8 3h1v1h-1zM19 3h1v1h-1zM6 4h1v1h-1zM11 4h1v1h-1zM16 4h1v1h-1zM21 4h1v1h-1zM5 5h1v1h-1zM10 5h1v1h-1zM17 5h1v1h-1zM22 5h1v1h-1zM4 6h4v1h-4zM11 6h1v1h-1zM16 6h1v1h-1zM20 6h4v1h-4zM9 7h2v1h-2zM17 7h2v1h-2zM3 8h1v1h-1zM24 8h1v1h-1zM2 10h1v1h-1zM11 10h1v1h-1zM16 10h1v1h-1zM25 10h1v1h-1zM2 11h1v1h-1zM25 11h1v1h-1zM2 12h1v1h-1zM4 12h1v1h-1zM8 12h2v1h-2zM18 12h2v1h-2zM23 12h1v1h-1zM25 12h1v1h-1zM2 13h3v1h-3zM23 13h3v1h-3zM3 14h2v1h-2zM23 14h2v1h-2zM8 16h1v1h-1zM19 16h1v1h-1zM3 17h1v1h-1zM5 17h1v1h-1zM11 17h1v1h-1zM16 17h1v1h-1zM22 17h1v1h-1zM24 17h1v1h-1zM2 18h1v1h-1zM6 18h1v1h-1zM21 18h1v1h-1zM25 18h1v1h-1zM13 19h2v1h-2zM2 20h1v1h-1zM25 20h1v1h-1zM3 21h1v1h-1zM6 21h1v1h-1zM9 21h1v1h-1zM18 21h1v1h-1zM21 21h1v1h-1zM24 21h1v1h-1zM4 22h2v1h-2zM11 22h6v1h-6zM22 22h2v1h-2zM10 25h8v1h-8zM10 26h1v1h-1zM17 26h1v1h-1zM12 28h1v1h-1zM15 28h1v1h-1zM15 29h1v1h-1zM15 30h1v1h-1z'], ['#e3c6c5', 'M9 3h10v1h-10zM7 4h4v1h-4zM12 4h4v1h-4zM17 4h4v1h-4zM6 5h4v1h-4zM12 5h4v1h-4zM18 5h4v1h-4zM12 6h4v1h-4zM11 7h6v1h-6zM10 8h8v1h-8zM3 9h1v1h-1zM10 9h8v1h-8zM24 9h1v1h-1zM3 10h1v1h-1zM10 10h1v1h-1zM17 10h1v1h-1zM24 10h1v1h-1zM3 11h1v1h-1zM10 11h1v1h-1zM17 11h1v1h-1zM24 11h1v1h-1zM3 12h1v1h-1zM10 12h1v1h-1zM17 12h1v1h-1zM24 12h1v1h-1zM8 13h3v1h-3zM17 13h3v1h-3zM8 14h3v1h-3zM17 14h3v1h-3zM8 15h3v1h-3zM17 15h3v1h-3zM9 16h2v1h-2zM17 16h2v1h-2zM4 17h1v1h-1zM9 17h2v1h-2zM17 17h2v1h-2zM23 17h1v1h-1zM3 18h3v1h-3zM9 18h10v1h-10zM22 18h3v1h-3zM2 19h5v1h-5zM9 19h4v1h-4zM15 19h4v1h-4zM21 19h5v1h-5zM3 20h4v1h-4zM9 20h10v1h-10zM21 20h4v1h-4zM4 21h2v1h-2zM10 21h8v1h-8zM22 21h2v1h-2zM11 26h6v1h-6zM10 27h8v1h-8zM10 28h2v1h-2zM16 28h2v1h-2zM10 29h3v1h-3zM16 29h2v1h-2zM11 30h2v1h-2zM16 30h1v1h-1z'], ['#b97371', 'M11 5h1v1h-1zM16 5h1v1h-1zM8 6h3v1h-3zM17 6h3v1h-3zM4 7h1v1h-1zM23 7h1v1h-1zM2 14h1v1h-1zM25 14h1v1h-1zM2 21h1v1h-1zM25 21h1v1h-1zM3 22h1v1h-1zM6 22h2v1h-2zM10 22h1v1h-1zM17 22h1v1h-1zM20 22h2v1h-2zM24 22h1v1h-1zM4 23h4v1h-4zM20 23h4v1h-4zM6 24h2v1h-2zM20 24h2v1h-2z'], ['#ffffff', 'M5 8h2v1h-2zM19 8h2v1h-2zM5 9h2v1h-2zM19 9h2v1h-2zM12 11h4v1h-4zM13 16h2v1h-2z'], ['#9cc8cc', 'M5 12h3v1h-3zM20 12h3v1h-3zM5 13h3v1h-3zM20 13h3v1h-3zM5 14h3v1h-3zM7 15h1v1h-1z'], ['#837f7f', 'M12 12h1v1h-1zM15 12h1v1h-1z'], ['#9cc8cd', 'M20 14h3v1h-3zM20 15h1v1h-1z']] },
9  limit: { width: 36, height: 27, paths: [['#080000', 'M20 0h6v1h-6zM17 1h12v1h-12zM15 2h6v1h-6zM25 2h6v1h-6zM14 3h4v1h-4zM22 3h1v1h-1zM28 3h4v1h-4zM13 4h3v1h-3zM23 4h1v1h-1zM30 4h3v1h-3zM11 5h4v1h-4zM21 5h1v1h-1zM24 5h1v1h-1zM31 5h2v1h-2zM9 6h5v1h-5zM21 6h2v1h-2zM32 6h2v1h-2zM7 7h7v1h-7zM21 7h2v1h-2zM32 7h3v1h-3zM4 8h5v1h-5zM11 8h2v1h-2zM21 8h2v1h-2zM33 8h2v1h-2zM3 9h5v1h-5zM11 9h2v1h-2zM21 9h2v1h-2zM33 9h2v1h-2zM2 10h3v1h-3zM6 10h2v1h-2zM10 10h3v1h-3zM21 10h1v1h-1zM34 10h2v1h-2zM2 11h2v1h-2zM6 11h2v1h-2zM10 11h2v1h-2zM18 11h3v1h-3zM34 11h2v1h-2zM1 12h7v1h-7zM10 12h2v1h-2zM18 12h1v1h-1zM20 12h2v1h-2zM34 12h2v1h-2zM0 13h2v1h-2zM6 13h2v1h-2zM10 13h2v1h-2zM18 13h1v1h-1zM20 13h3v1h-3zM34 13h2v1h-2zM0 14h2v1h-2zM6 14h2v1h-2zM10 14h2v1h-2zM18 14h1v1h-1zM20 14h3v1h-3zM34 14h2v1h-2zM0 15h2v1h-2zM6 15h2v1h-2zM10 15h2v1h-2zM18 15h1v1h-1zM20 15h3v1h-3zM34 15h2v1h-2zM0 16h2v1h-2zM7 16h2v1h-2zM10 16h3v1h-3zM18 16h1v1h-1zM20 16h2v1h-2zM34 16h2v1h-2zM0 17h13v1h-13zM18 17h3v1h-3zM33 17h2v1h-2zM1 18h12v1h-12zM33 18h2v1h-2zM1 19h2v1h-2zM6 19h2v1h-2zM11 19h3v1h-3zM21 19h2v1h-2zM32 19h3v1h-3zM1 20h2v1h-2zM6 20h2v1h-2zM12 20h2v1h-2zM21 20h2v1h-2zM25 20h1v1h-1zM32 20h2v1h-2zM2 21h2v1h-2zM6 21h2v1h-2zM13 21h2v1h-2zM21 21h1v1h-1zM24 21h1v1h-1zM31 21h2v1h-2zM3 22h7v1h-7zM14 22h2v1h-2zM21 22h1v1h-1zM23 22h1v1h-1zM30 22h3v1h-3zM4 23h14v1h-14zM28 23h4v1h-4zM10 24h11v1h-11zM25 24h6v1h-6zM17 25h12v1h-12zM19 26h7v1h-7z'], ['#cf9c9b', 'M21 2h4v1h-4zM18 3h1v1h-1zM21 3h1v1h-1zM23 3h1v1h-1zM27 3h1v1h-1zM16 4h1v1h-1zM21 4h2v1h-2zM24 4h1v1h-1zM29 4h1v1h-1zM15 5h1v1h-1zM22 5h2v1h-2zM30 5h1v1h-1zM14 6h1v1h-1zM24 6h1v1h-1zM31 6h1v1h-1zM9 8h2v1h-2zM13 8h1v1h-1zM32 8h1v1h-1zM8 9h2v1h-2zM22 10h1v1h-1zM33 10h1v1h-1zM12 11h1v1h-1zM21 11h1v1h-1zM33 11h1v1h-1zM12 12h1v1h-1zM22 12h1v1h-1zM2 13h1v1h-1zM5 13h1v1h-1zM12 13h1v1h-1zM12 14h1v1h-1zM5 15h1v1h-1zM8 15h2v1h-2zM13 15h1v1h-1zM2 16h4v1h-4zM13 16h1v1h-1zM22 16h1v1h-1zM33 16h1v1h-1zM13 17h3v1h-3zM21 17h2v1h-2zM31 17h2v1h-2zM14 18h7v1h-7zM23 18h1v1h-1zM29 18h3v1h-3zM4 19h2v1h-2zM8 19h3v1h-3zM14 19h7v1h-7zM23 19h9v1h-9zM4 20h2v1h-2zM15 20h5v1h-5zM23 20h1v1h-1zM26 20h5v1h-5zM16 21h3v1h-3zM23 21h1v1h-1zM26 21h4v1h-4zM17 22h1v1h-1zM25 22h4v1h-4zM23 23h4v1h-4zM23 24h1v1h-1z'], ['#e3c6c5', 'M19 3h2v1h-2zM24 3h3v1h-3zM17 4h3v1h-3zM25 4h4v1h-4zM16 5h3v1h-3zM25 5h5v1h-5zM15 6h4v1h-4zM23 6h1v1h-1zM25 6h6v1h-6zM14 7h4v1h-4zM23 7h9v1h-9zM14 8h4v1h-4zM23 8h9v1h-9zM13 9h5v1h-5zM20 9h1v1h-1zM23 9h10v1h-10zM8 10h2v1h-2zM13 10h5v1h-5zM20 10h1v1h-1zM23 10h10v1h-10zM8 11h2v1h-2zM13 11h5v1h-5zM22 11h11v1h-11zM8 12h2v1h-2zM13 12h5v1h-5zM23 12h11v1h-11zM3 13h1v1h-1zM8 13h2v1h-2zM13 13h5v1h-5zM23 13h11v1h-11zM2 14h4v1h-4zM8 14h2v1h-2zM13 14h5v1h-5zM23 14h11v1h-11zM2 15h3v1h-3zM14 15h4v1h-4zM23 15h11v1h-11zM14 16h4v1h-4zM23 16h10v1h-10zM16 17h2v1h-2zM23 17h8v1h-8zM24 18h5v1h-5z'], ['#68b2b4', 'M20 4h1v1h-1zM20 8h1v1h-1zM18 10h1v1h-1zM21 23h1v1h-1z'], ['#9acccd', 'M19 5h2v1h-2zM19 6h2v1h-2zM18 7h3v1h-3zM18 8h2v1h-2zM18 9h2v1h-2zM19 10h1v1h-1zM20 20h1v1h-1zM19 21h2v1h-2zM18 22h3v1h-3zM18 23h3v1h-3zM21 24h2v1h-2z'], ['#b97371', 'M10 9h1v1h-1zM5 10h1v1h-1zM4 11h2v1h-2zM12 15h1v1h-1zM6 16h1v1h-1zM9 16h1v1h-1zM13 18h1v1h-1zM21 18h2v1h-2zM32 18h1v1h-1zM3 19h1v1h-1zM3 20h1v1h-1zM8 20h4v1h-4zM14 20h1v1h-1zM24 20h1v1h-1zM31 20h1v1h-1zM4 21h2v1h-2zM8 21h5v1h-5zM15 21h1v1h-1zM22 21h1v1h-1zM25 21h1v1h-1zM30 21h1v1h-1zM10 22h4v1h-4zM16 22h1v1h-1zM22 22h1v1h-1zM24 22h1v1h-1zM29 22h1v1h-1zM22 23h1v1h-1zM27 23h1v1h-1zM24 24h1v1h-1z'], ['#ffffff', 'M19 12h1v1h-1zM19 13h1v1h-1zM19 14h1v1h-1zM19 15h1v1h-1zM19 16h1v1h-1z'], ['#efe0df', 'M4 13h1v1h-1z']] },
10} satisfies Record<string, Sprite>
11
types/index.d.ts 8 lines
1export type Reading = { percentUsed: number; resetsAt: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'usage-pace': { fiveHour: Reading | null; sevenDay: Reading | null }
6  }
7}
8