ModsCode / Gallery / Designs / Notification stack
Notification stack
The latest notifications as a receding stack of cards, newest on top; a left stripe and a label name the kind, and older cards inset and fade.


hooks/designs/notify-stack.mjs
// notify-stack: the four latest Claude Code notifications, newest on top, as kanban's cards (1px outline
// at 18% ink, no fill, a 2px stripe inside the left edge that stops 6px short of the corners). Each older
// card is inset 6px more on both sides and drawn at 100 / 82 / 65 / 50% opacity as one group (no lower: at 25% the muted line
// of the oldest card vanished at thumbnail size), so the
// stripe, outline and words fade together and the list recedes like a stack. The stripe and the small
// label on the right name the kind: needs you (Claude orange, the one accent: a permission prompt waiting on the
// person), failed (red), done (green) and info (ink). Title 10px ink, one line and the age 9px muted.
// Sample data only.
const W = 380
const H = 196
const STAGE = '#FAF9F5'
const INK = '#141413'
const MUTED = '#73726C'
const UP = '#2C7A39'
const DOWN = '#AB2B3F'
const CLAUDE = '#D77757'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const MONO = "ui-monospace,'Cascadia Mono',Consolas,monospace"
const f = n => n.toFixed(1)
const CARD = { x0: 12, y0: 12, h: 36, gap: 6, inset: 6 }
const FADE = [1, 0.82, 0.65, 0.5]
const KIND = {
needs: { label: 'NEEDS YOU', stripe: CLAUDE, opacity: 1 },
failed: { label: 'FAILED', stripe: DOWN, opacity: 1 },
done: { label: 'DONE', stripe: UP, opacity: 1 },
info: { label: 'INFO', stripe: INK, opacity: 0.65 },
}
// Newest first. A body in mono is a command or a path; the rest is prose.
const NOTES = [
{ kind: 'needs', title: 'Approve a command', body: 'git push origin main', mono: true, age: 'NOW' },
{ kind: 'failed', title: 'Build failed', body: 'tsc found 3 errors in src/api/retry.ts', age: '2m' },
{ kind: 'done', title: 'Refactor finished', body: '12 files changed, 214 tests passed, $0.42', age: '9m' },
{ kind: 'info', title: 'Context compacted', body: '71% to 18%, the summary stays in the session', age: '24m' },
]
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
const text = (x, y, s, { size = 9, fill = MUTED, anchor = 'start', font = FONT } = {}) =>
`<text x="${f(x)}" y="${f(y)}"${anchor === 'start' ? '' : ` text-anchor="${anchor}"`} font-size="${size}" font-family="${font}" fill="${fill}">${esc(s)}</text>`
function card(n, i) {
const k = KIND[n.kind]
const x = CARD.x0 + i * CARD.inset
const w = W - 2 * x
const y = CARD.y0 + i * (CARD.h + CARD.gap)
return [
`<g opacity="${FADE[i]}">`,
`<rect x="${f(x + 0.5)}" y="${f(y + 0.5)}" width="${f(w - 1)}" height="${CARD.h - 1}" rx="3" fill="none" stroke="${INK}" stroke-opacity="0.18"/>`,
`<rect x="${f(x + 1)}" y="${f(y + 6)}" width="2" height="${CARD.h - 12}" fill="${k.stripe}"${k.opacity < 1 ? ` fill-opacity="${k.opacity}"` : ''}/>`,
text(x + 12, y + 15, n.title, { size: 10, fill: INK }),
text(x + w - 10, y + 15, k.label, { fill: n.kind === 'info' ? MUTED : k.stripe, anchor: 'end' }),
text(x + 12, y + 28, n.body, { font: n.mono ? MONO : FONT }),
text(x + w - 10, y + 28, n.age, { anchor: 'end' }),
'</g>',
].join('')
}
function draw() {
const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
NOTES.forEach((n, i) => parts.push(card(n, i)))
parts.push(text(12, H - 8, 'SAMPLE NOTIFICATIONS'))
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}
let source // drawn on first use and kept: the pane loads every design, so none pays at load time
export default {
id: 'notify-stack',
title: 'Notification stack',
category: 'work',
description: 'The latest notifications as a receding stack of cards, newest on top; a left stripe and a label name the kind, and older cards inset and fade.',
element: ui => ui.Svg({
source: (source ??= draw()),
alt: `A stack of ${NOTES.length} notifications, newest on top: ${NOTES.map(n => n.title.toLowerCase()).join(', ')}, older ones fading, sample data`,
width: W,
height: H,
}),
}
To use it in a mod: copy the file to your mod's hooks/designs/ and return its element(ui) from a pane's ui.render. How a pane is built.
Code: PolyForm Noncommercial License 1.0.0: noncommercial use only. Required Notice: Copyright 2026 Haruki Nakada (https://modscode.com).