Skip to content

DirectorySkills

Goal rings dashboard: 1440, made in this run
Run 1Goal rings dashboard“Make a dark dashboard for our gym chain's owners showing progress to this month's targets. Details in brief.md.”
Runs in
  • Claude Code
  • Codex
  • Cursor
  • Gemini CLI
  • GitHub Copilot
  • OpenCode
Generates with
Your agent's model
Tested with
Claude Code 2.1.322Claude Sonnet 5.51 real run, 10 Nov
Needs
Node 20+Chrome
Licence
Recaply licence

Goal Rings Dashboard

A dark admin dashboard built around goal rings: an icon rail, three ring goals, a revenue area chart, plan mix, accounts and activity.

$9.99once, yours to keep

Builds a dark admin dashboard with goal rings into your own project: deep navy screen with a narrow icon rail; a greeting and a This month / This quarter switch; three cards each with a glowing progress ring (revenue goal, new trials, churn kept under its limit); a smooth revenue area chart with hover readout; plan mix bars; top accounts with plan chips; an activity feed with a Mark all read button. Plain HTML, CSS and JavaScript, or matched to React, Next.js, Vue, Svelte, Astro or Tailwind when the project uses them. Comes with a working reference and a check that screenshots it at phone, tablet and desktop widths and flags sideways overflow, text under 12px, missing alt text and names, low contrast, no focus styles, motion that ignores reduced motion, leftover sample words and emoji icons. Use when a SaaS, subscription, membership or creator business wants a striking dark admin home that shows progress to goals, or someone asks for a dark dashboard, analytics admin, ring or radial progress dashboard.

By RecaplyUpdated 12 Nov

Install Goal Rings Dashboard

After the download, ask your agent to add it, or run the line yourself.

Read https://recaply.co/skills/goal-rings-dashboard/install.md and follow it to install the goal-rings-dashboard skill from goal-rings-dashboard.zip.

Paste it into your agent. It reads our install steps, finds the ZIP you downloaded and adds the skill for you.

New to skills? How to install a skill

Similar skills

All 735 skills

Who Goal Rings Dashboard is for

What you get

  • One HTML file with its CSS inside: no build step, no framework
  • A dark admin dashboard with goal rings
  • Checked at phone and desktop widths

Good fit

  • When a SaaS, subscription, membership or creator business wants a striking dark admin home that shows progress to goals

Not for

  • An app with a backend, sign-in or a database
  • Copying another company's site

About this skill

You say

Make a dark dashboard for our gym chain's owners showing progress to this month's targets. Details in brief.md.

It said back

The dashboard is built, but every number on it is sample data. The brief has your targets and layout but no real figures or site names. The automated checks pass at phone, tablet and desktop widths, and I looked at the phone and desktop screenshots. I didn't look at the tablet one.

It's black with electric orange and white accents, with a slim icon rail down the left.

Show more

What it did in run 1

  1. ./brief.md
  2. goal-rings-dashboard
  3. cd .; ls -la; S=.claude/skills/goal-rings-dashboard; ls $S $S/reference; wc -c $S/reference/*; cat $S/reference/index.html
  4. cd ./.claude/skills/goal-rings-dashboard/reference; cat style.css script.js
  5. ./.claude/skills/goal-rings-dashboard/reference/index.html
  6. cd . && R=.claude/skills/goal-rings-dashboard/reference && cp $R/style.css style.css && cd . && sed -i '' -e 's/#0A0D1C/#000000/g; s/--surface:#131834/--surface:#111111/; s/--surface-2:#1B2147/--surface-2:#1E1E1E/; s/--ink:#EEF0FF/--ink:#FFFFFF/; s/--muted:#A4ABD4/--muted:#B3B3B3/; s/--line:#262D5A/--line:#2E2E2E/; s/--accent:#8B7CFF/--accent:#FF5A1F/; s/--em:#B3A8FF/--em:#FF8A5C/; s/--accent2:#38D1F0/--accent2:#FFFFFF/; s/--accent3:#B6F23A/--accent3:#FFB27A/; s/#0E1229/#0A0A0A/; s/#2A2560/#3A1608/; s/#252B57/#262626/; s/#202650/#262626/; s/#232A55/#262626/; s/#3A4280/#555/; s/#C9C2FF/#FFB27A/; s/rgba(56,209,240,.15)/rgba(255,90,31,.2)/' style.css && sed -i '' 's/--on-accent:#000000/--on-accent:#000000/; s/.act li.un .ud{background:var(--accent2)/.act li.un .ud{background:var(--accent)/' style.css cat >> style.css <<'E' .rings .ring-card:nth-child(2) .val{filter:drop-shadow(0 0 6px rgba(255,255,255,.35))} .ring .val{filter:drop-shadow(0 0 6px color-mix(in srgb,var(--c) 55%,transparent))} .up.dn{color:var(--bad)} .tag{display:inline-block;margin-left:6px;padding:1px 8px;border-radius:999px;font-size:12px;font-weight:700;background:var(--surface-2)} .tag.neg{color:var(--bad)}.tag.pos{color:var(--good)} .note{font-size:12px;color:var(--muted);padding:0 clamp(16px,3vw,32px) 24px} .sites li{display:grid;grid-template-columns:1fr auto;gap:6px 12px;padding:10px 0;border-top:1px solid var(--line)} .sites li:first-child{border-top:0} .sites .mb{grid-column:1/-1} .mt td:last-child,.mt th:last-child{text-align:right} @media (max-width:900px){.top{grid-template-columns:1fr auto}} E cat > index.html <<'E' <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Ironleaf Gyms – November targets</title> <meta name="description" content="Owner dashboard for Ironleaf Gyms: progress to this month's targets across all four sites."> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;700&display=swap"> <link rel="stylesheet" href="style.css"> </head> <body> <a class="skip" href="#main">Skip to content</a> <div class="adm orb"><aside class="side" id="side" aria-label="Main menu"><p class="logo" aria-label="Ironleaf Gyms"><span aria-hidden="true">I</span></p><nav> <a href="#main" aria-current="page"><svg class="ic" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 11l9-7 9 7v9H3zM9 20v-6h6v6"/></svg><span>Home</span></a> <a href="#main"><svg class="ic" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M16 19v-1a4 4 0 0 0-8 0v1M12 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6M20 19v-1a3 3 0 0 0-2-2.8M17 5.2a3 3 0 0 1 0 5.6"/></svg><span>Members</span></a> <a href="#main"><svg class="ic" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/></svg><span>Classes</span></a> <a href="#main"><svg class="ic" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 21s-7-6-7-11a7 7 0 0 1 14 0c0 5-7 11-7 11M12 12a2 2 0 1 0 0-4 2 2 0 0 0 0 4"/></svg><span>Sites</span></a> </nav></aside> <main id="main"><header class="top"><div class="hi"><h1>November targets</h1><p>Ironleaf Gyms, all 4 sites · <span data-when></span></p></div><button class="menu-btn" type="button" data-nav-toggle aria-controls="side" aria-expanded="false" aria-label="Menu"><svg class="ic" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 7h16M4 12h16M4 17h16"/></svg></button></header> <div class="orb-in"><div class="rings" data-rings></div> <div class="row2"><section class="card" aria-labelledby="rev-h"><div class="card-h"><div><h2 id="rev-h">Members per month</h2><p class="big num" data-mrr></p></div><p class="rd num" aria-hidden="true" data-read></p></div><div class="area" data-area role="img" aria-label="Members per month"><svg aria-hidden="true" focusable="false"></svg></div><ol class="mons" data-mons aria-hidden="true"></ol><details class="figs"><summary>Show figures</summary><table class="mt"><caption class="sr-only">Members by month</caption><thead><tr><th scope="col">Month</th><th scope="col">Members</th></tr></thead><tbody data-figs></tbody></table></details></section> <section class="card" aria-labelledby="mix-h"><h2 id="mix-h">Membership types</h2><p class="muted">Active members by type</p><ul class="mix" data-mix></ul><p class="muted tot" data-mixtot></p></section></div> <div class="row3"><section class="card" aria-labelledby="site-h"><h2 id="site-h">Busiest sites</h2><p class="muted">Visits so far this month</p><ul class="sites" data-sites></ul></section> <section class="card" aria-labelledby="act-h"><div class="card-h"><h2 id="act-h">Sign-ups and cancellations</h2><button class="lnk" type="button" data-read-all>Mark all read</button></div><p class="sr-only" aria-live="polite" data-act-live></p><ul class="act" data-act></ul></section></div></div> <p class="note">Placeholder figures: every number, site name and person shown here is sample data until it is connected to Ironleaf's membership system. Targets (300 / 5,000 / 85%) come from the brief.</p></main></div> <script src="script.js" defer></script> </body> </html> E cat > script.js <<'E' document.documentElement.classList.add('js'); const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); for (const b of document.querySelectorAll('[data-nav-toggle]')) { const menu = document.getElementById(b.getAttribute('aria-controls')); const set = (o) => { b.setAttribute('aria-expanded', String(o)); menu.classList.toggle('open', o); }; b.addEventListener('click', () => set(b.getAttribute('aria-expanded') !== 'true')); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && b.getAttribute('aria-expanded') === 'true') { set(false); b.focus(); } }); menu.addEventListener('click', (e) => { if (e.target.closest('a')) set(false); }); } // SAMPLE DATA: replace with real figures from the membership system. const DATA = { when: '10 days in, 20 to go · sample data', targets: [ { id: 'r1', name: 'New members', color: 'var(--accent)', value: 168, goal: 300, fmt: (n) => n.toLocaleString('en-GB'), note: 'Need about 7 a day to hit 300' }, { id: 'r2', name: 'Classes booked', color: 'var(--accent2)', value: 2140, goal: 5000, fmt: (n) => n.toLocaleString('en-GB'), note: 'Need about 143 a day to hit 5,000' }, { id: 'r3', name: 'Members who visited', color: 'var(--accent3)', value: 61, goal: 85, unit: '%', fmt: (n) => n + '%', note: 'Goal is 85% of members visiting' } ], months: [['Apr', 2410], ['May', 2475], ['Jun', 2530], ['Jul', 2498], ['Aug', 2562], ['Sep', 2690], ['Oct', 2744], ['Nov', 2790]], types: [['Full', 1180, 'var(--accent)'], ['Off-peak', 820, 'var(--accent2)'], ['Student', 460, 'var(--accent3)'], ['Family', 330, 'var(--em)']], sites: [['Site 1', 4120], ['Site 2', 3560], ['Site 3', 2980], ['Site 4', 2210]], activity: [ ['in', 'Sign-up', 'Full membership, Site 1.', '12 min ago', 1], ['in', 'Sign-up', 'Family membership, Site 3.', '48 min ago', 1], ['out', 'Cancellation', 'Off-peak membership, Site 2.', '2 hours ago', 1], ['in', 'Sign-up', 'Student membership, Site 4.', '4 hours ago', 0], ['out', 'Cancellation', 'Full membership, Site 1.', 'Yesterday', 0] ] }; const $ = (s) => document.querySelector(s); const ringSvg = '<svg viewBox="0 0 120 120" aria-hidden="true" focusable="false"><circle cx="60" cy="60" r="50" class="trk"/><circle cx="60" cy="60" r="50" class="val" pathLength="100"/></svg>'; $('[data-when]').textContent = DATA.when; $('[data-rings]').innerHTML = DATA.targets.map((t) => <section class="card ring-card" style="--c:${t.color}" aria-labelledby="${t.id}-h"><div class="ring">${ringSvg}<span class="pct num">${Math.round(t.value / t.goal * 100)}%</span></div><div><h2 id="${t.id}-h">${t.name}</h2><p class="rv num">${t.unit ? t.value + '% of members, goal ' + t.goal + '%' : t.fmt(t.value) + ' of ' + t.fmt(t.goal)}</p><p class="rn">${t.note}</p><p class="sr-only">${Math.round(t.value / t.goal * 100)}% of the way to target</p></div></section>).join(''); requestAnimationFrame(() => document.querySelectorAll('.ring-card').forEach((c, i) => { const t = DATA.targets[i]; c.querySelector('.val').style.strokeDasharray = Math.min(100, t.value / t.goal * 100) + ' 100'; })); const tot = DATA.types.reduce((a, t) => a + t[1], 0), max = Math.max(...DATA.types.map((t) => t[1])); $('[data-mix]').innerHTML = DATA.types.map((t) => <li style="--c:${t[2]};--w:${t[1] / max * 100}%"><span class="mn">${t[0]}</span><span class="mb" aria-hidden="true"><i></i></span><span class="mv num">${t[1].toLocaleString('en-GB')}</span></li>).join(''); $('[data-mixtot]').textContent = tot.toLocaleString('en-GB') + ' active members'; const smax = DATA.sites[0][1]; $('[data-sites]').innerHTML = DATA.sites.map((s) => <li><b>${s[0]}</b><span class="mv num">${s[1].toLocaleString('en-GB')} visits</span><span class="mb" aria-hidden="true" style="--c:var(--accent);--w:${s[1] / smax * 100}%"><i></i></span></li>).join(''); $('[data-act]').innerHTML = DATA.activity.map((a) => <li class="${a[4] ? 'un' : ''}"><span class="ud" aria-hidden="true"></span><span><b>${a[1]}<span class="tag ${a[0] === 'in' ? 'pos' : 'neg'}">${a[0] === 'in' ? 'joined' : 'left'}</span></b>${a[2]}<small>${a[3]}${a[4] ? '<span class="sr-only">, unread</span>' : ''}</small></span></li>).join(''); $('[data-read-all]').addEventListener('click', () => { const un = document.querySelectorAll('.act li.un'); un.forEach((li) => { li.classList.remove('un'); li.querySelector('small .sr-only')?.remove(); }); $('[data-act-live]').textContent = un.length ? un.length + ' items marked as read.' : 'Nothing unread.'; }); const area = $('[data-area]'), svg = area.querySelector('svg'), NS = 'http://www.w3.org/2000/svg', readEl = $('[data-read]'), s = DATA.months; let pts = []; function el(t, a) { const e = document.createElementNS(NS, t); for (const k in a) e.setAttribute(k, a[k]); svg.appendChild(e); return e; } function chart(animate) { const w = area.clientWidth, h = area.clientHeight, pad = 14; svg.setAttribute('viewBox', 0 0 ${w} ${h}); svg.replaceChildren(); const vals = s.map((x) => x[1]), lo = Math.min(...vals) * 0.97, hi = Math.max(...vals) * 1.01; const X = (i) => pad + i * (w - pad * 2) / (s.length - 1), Y = (v) => h - 10 - (v - lo) / (hi - lo) * (h - 30); for (let g = 0; g < 4; g++) el('line', { class: 'gl', x1: 0, x2: w, y1: 20 + g * (h - 30) / 3, y2: 20 + g * (h - 30) / 3 }); pts = s.map((x, i) => [X(i), Y(x[1])]); let d = M${pts[0][0]} ${pts[0][1]}; for (let i = 1; i < pts.length; i++) { const p = pts[i - 1], q = pts[i], cx = (p[0] + q[0]) / 2; d += C${cx} ${p[1]} ${cx} ${q[1]} ${q[0]} ${q[1]}; } const defs = el('defs', {}), lg = document.createElementNS(NS, 'linearGradient'); lg.setAttribute('id', 'orbg'); lg.setAttribute('x1', 0); lg.setAttribute('x2', 0); lg.setAttribute('y1', 0); lg.setAttribute('y2', 1); lg.innerHTML = '<stop offset="0" stop-color="#FF5A1F" stop-opacity=".45"/><stop offset="1" stop-color="#FF5A1F" stop-opacity="0"/>'; defs.appendChild(lg); el('path', { d: ${d}L${pts.at(-1)[0]} ${h}L${pts[0][0]} ${h}Z, fill: 'url(#orbg)' }); const ln = el('path', { class: 'ln', d }); if (animate && !reduceMotion.matches) { const L = ln.getTotalLength(); ln.style.strokeDasharray = L; ln.style.strokeDashoffset = L; ln.getBoundingClientRect(); ln.style.transition = 'stroke-dashoffset .8s ease'; ln.style.strokeDashoffset = 0; } el('circle', { class: 'pt', cx: pts.at(-1)[0], cy: pts.at(-1)[1], r: 6 }); $('[data-mons]').innerHTML = s.map((x) => <li>${x[0]}</li>).join(''); $('[data-figs]').innerHTML = s.map((x) => <tr><td>${x[0]}</td><td>${x[1].toLocaleString('en-GB')}</td></tr>).join(''); area.setAttribute('aria-label', Members per month, from ${s[0][1].toLocaleString('en-GB')} in ${s[0][0]} to ${s.at(-1)[1].toLocaleString('en-GB')} in ${s.at(-1)[0]}.); } const delta = s.at(-1)[1] - s.at(-2)[1]; $('[data-mrr]').innerHTML = ${s.at(-1)[1].toLocaleString('en-GB')} <span class="up ${delta < 0 ? 'dn' : ''}">${delta >= 0 ? '+' : ''}${delta} on ${s.at(-2)[0]}</span>; area.addEventListener('pointermove', (e) => { const x = e.clientX - area.getBoundingClientRect().left; let k = 0; pts.forEach((p, i) => { if (Math.abs(p[0] - x) < Math.abs(pts[k][0] - x)) k = i; }); readEl.innerHTML = ${s[k][0]}<b>${s[k][1].toLocaleString('en-GB')}</b>; let c = svg.querySelector('.cur'); if (!c) c = el('line', { class: 'cur', y1: 0 }); c.setAttribute('x1', pts[k][0]); c.setAttribute('x2', pts[k][0]); c.setAttribute('y2', area.clientHeight); }); area.addEventListener('pointerleave', () => { readEl.innerHTML = ''; svg.querySelector('.cur')?.remove(); }); requestAnimationFrame(() => chart(true)); if ('ResizeObserver' in window) { let w0 = 0; new ResizeObserver(() => { if (area.clientWidth !== w0) { w0 = area.clientWidth; chart(false); } }).observe(area); } E node .claude/skills/goal-rings-dashboard/scripts/check.mjs index.html --out check 2>&1 | tail -30
  7. cd . && cp .claude/skills/goal-rings-dashboard/reference/style.css style.css && sed -i '' -e 's/#0A0D1C/#000000/g; s/--surface:#131834/--surface:#111111/; s/--surface-2:#1B2147/--surface-2:#1E1E1E/; s/--ink:#EEF0FF/--ink:#FFFFFF/; s/--muted:#A4ABD4/--muted:#B3B3B3/; s/--line:#262D5A/--line:#2E2E2E/; s/--accent:#8B7CFF/--accent:#FF5A1F/; s/--em:#B3A8FF/--em:#FF8A5C/; s/--accent2:#38D1F0/--accent2:#FFFFFF/; s/--accent3:#B6F23A/--accent3:#FFB27A/; s/#0E1229/#0A0A0A/; s/#2A2560/#3A1608/; s/#252B57/#262626/; s/#202650/#262626/; s/#232A55/#262626/; s/#3A4280/#555/; s/#C9C2FF/#FFB27A/; s/rgba(56,209,240,.15)/rgba(255,90,31,.2)/; s/.act li.un .ud{background:var(--accent2)/.act li.un .ud{background:var(--accent)/' style.css && grep -c FF5A1F style.css
  8. ./extra.css
  9. cp .claude/skills/goal-rings-dashboard/reference/style.css style.css
  10. W=.; cp $W/.claude/skills/goal-rings-dashboard/reference/style.css $W/style.css && ls $W
  11. ./style.css
  12. ./index.html
  13. ./script.js
  14. W=.; rm -f $W/extra.css; node $W/.claude/skills/goal-rings-dashboard/scripts/check.mjs $W/index.html --out $W/check 2>&1 | tail -30
  15. node ./.claude/skills/goal-rings-dashboard/scripts/check.mjs ./index.html --out ./check 2>&1 | tail -30
  16. ./check/1440.png
  17. ./check/390.png
  18. Done

10 Nov 2026 · Claude Code 2.1.322 with Claude Sonnet 5.5 · 2 min 19 s · $0.45 of API use

Questions

Is Goal Rings Dashboard free?

No. Goal Rings Dashboard costs $9.99 once and stays in your Recaply account, or it comes with the Recaply plan ($19.99 a month), which opens every premium skill and prompt.

Which agents can use Goal Rings Dashboard?

Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode.

What do I get when I buy Goal Rings Dashboard?

A ZIP of the whole skill folder (36 KB): the SKILL.md your agent follows and its 6 other files, the references, templates and scripts it uses. You download it from this page whenever you need it, and install it in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode. The Recaply plan opens it too, with every other premium skill and prompt.

How do I install Goal Rings Dashboard?

Buy it, then download the ZIP from this page. For Claude Code, unzip it into .claude/skills/ in your project. For Codex, Cursor and Gemini CLI, unzip it and run npx skills add ./goal-rings-dashboard with your agent's flag.

What do I get from Goal Rings Dashboard?

One HTML file with its CSS inside: no build step, no framework. A dark admin dashboard with goal rings. Checked at phone and desktop widths.

What does Goal Rings Dashboard need?

Node 20+ and Chrome, and no API key. Each use costs nothing beyond your agent.

How big is Goal Rings Dashboard?

7 files, about 24,043 tokens in all. The SKILL.md is about 2,295 tokens, and your agent reads it only when the job fits.

What does a run cost?

Our 1 runs took 2 min 19 s to 2 min 19 s and $0.45 to $0.45 of API use each, in Claude Code with claude-sonnet-5-5. On a Claude plan, a run counts toward your usage instead.

Do I have to name Goal Rings Dashboard to use it?

Type /goal-rings-dashboard first to make sure it loads. Without it, Claude Code may judge a small job simple enough to do without the skill.