Video
![]()
![]()
Editorial hero for a report




An editable spreadsheet-style grid: lettered columns, numbered rows, a formula bar, keyboard moves and live totals.
$9.99once, yours to keep
Payments are non-refundable. See our Terms of service.
Yours. Download it, then add it to your agent.
Builds an editable spreadsheet-style data grid into your own project: a sheet with letters across the top and row numbers down the side; a formula bar shows the chosen cell's name and value or formula; arrow keys move the green selection, Enter or typing edits, Escape cancels; "Left" and the totals row work themselves out, overspends show in red with a minus sign. 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 product needs people to edit numbers in a familiar sheet (budgets, quotes, rotas, stock counts), or someone asks for a spreadsheet, editable grid or Excel-like table.
By RecaplyUpdated 12 Nov
Video
![]()
![]()
Editorial hero for a report
Video1 view, 0 downloads
![]()
![]()
Lavender page for an AI app
Video
![]()
![]()
Product page for headphones
Video
![]()
![]()
Page with a node-flow hero
You say
We need an editable grid for staff hours this week, like a spreadsheet. Details in brief.md.
It said back
The staff hours grid is built as plain HTML, CSS and JavaScript, since the folder had no framework. The automated check passes at 390, 768 and 1440px: no sideways overflow, no text under 12px, and contrast, focus styles and reduced-motion handling are all fine. I looked at the 1440px screenshot and it reads cleanly.
I did not test the over-40 flag by typing hours in, so that part is untested. I also haven't opened the 390px and 768px screenshots myself.
What it did in run 1
<th scope="col" data-col="${l}">${l}</th>).join('') + '</tr></thead><tbody>'; for (let r = 1; r <= BOTTOM; r++) { html += <tr${r === BOTTOM ? ' class="tot"' : ''}><th scope="row">${r}</th>; for (let c = 0; c <= LAST; c++) { const label = r === 1, calc = !label && (c >= TOT || r === BOTTOM) && c > 0; const cls = [label || r === BOTTOM && c === 0 ? 'lab' : '', c > 0 && !label ? 'n' : '', calc ? 'f' : '', c === TOT ? 'tc' : '', c === LAST ? 'pc' : ''].join(' ').trim(); const text = label ? HEAD[c] : c === 0 ? (r === BOTTOM ? 'Total' : STAFF[r - FIRST]) : ''; html += <td role="gridcell" ${cls ? class="${cls}" : ''}${calc ? 'aria-readonly="true" ' : ''}${label || r === BOTTOM && c === 0 ? 'aria-readonly="true" ' : ''}data-r="${r}" data-c="${c}" tabindex="${r === FIRST && c === 0 ? 0 : -1}">${text}</td>; } html += '</tr>'; } grid.innerHTML = html + '</tbody>'; const cell = (r, c) => grid.querySelector(td[data-r="${r}"][data-c="${c}"]); const val = (r, c) => Number(cell(r, c).dataset.v || 0); function setCalc(c, text, v, over) { c.dataset.v = v; c.classList.toggle('neg', !!over); c.textContent = ''; if (over) { const s = document.createElement('span'); s.className = 'flag'; s.textContent = 'Over ' + LIMIT; c.append(s); } c.append(text); } function recalc() { const col = Array(LAST + 1).fill(0); for (let r = FIRST; r <= LASTSTAFF; r++) { let t = 0; for (let c = 1; c <= 7; c++) { const v = val(r, c); t += v; col[c] += v; } setCalc(cell(r, TOT), hrs(t), t, t > LIMIT); setCalc(cell(r, LAST), pay(t * RATE), t * RATE); col[TOT] += t; } for (let c = 1; c <= 7; c++) setCalc(cell(BOTTOM, c), hrs(col[c]), col[c]); setCalc(cell(BOTTOM, TOT), hrs(col[TOT]), col[TOT]); setCalc(cell(BOTTOM, LAST), pay(col[TOT] * RATE), col[TOT] * RATE); } recalc(); let cur = cell(FIRST, 0), editing = null; function formula(c) { const r = +c.dataset.r, k = +c.dataset.c; if (r === BOTTOM && k > 0) return k === LAST ? =I${BOTTOM}*${RATE} : =SUM(${L[k]}${FIRST}:${L[k]}${LASTSTAFF}); if (r >= FIRST && r <= LASTSTAFF) { if (k === TOT) return =SUM(B${r}:H${r}); if (k === LAST) return =I${r}*${RATE}; } return k > 0 && r > 1 ? (c.dataset.v ? String(c.dataset.v) : '') : c.textContent; } function describe(c) { let s = L[+c.dataset.c] + c.dataset.r + ': ' + (c.textContent.replace(/^Over \d+/, '').trim() || 'empty'); if (c.classList.contains('neg')) s += ', over ' + LIMIT + ' hours'; return s + (c.getAttribute('aria-readonly') === 'true' ? ', read only' : ''); } function select(c, focus) { if (!c) return; cur.tabIndex = -1; cur = c; c.tabIndex = 0; if (focus !== false) c.focus(); ref.textContent = L[+c.dataset.c] + c.dataset.r; fx.textContent = formula(c); grid.querySelectorAll('th.on').forEach((h) => h.classList.remove('on')); grid.querySelector(thead th[data-col="${L[+c.dataset.c]}"]).classList.add('on'); c.parentElement.querySelector('th').classList.add('on'); if (focus !== false) live.textContent = describe(c); } const editable = (c) => c.getAttribute('aria-readonly') !== 'true'; function edit(c, start) { if (!editable(c) || editing) return; const isN = c.classList.contains('n'), old = isN ? (c.dataset.v || '') : c.textContent; const inp = document.createElement('input'); inp.value = start !== undefined ? start : old; inp.setAttribute('aria-label', 'Edit ' + L[+c.dataset.c] + c.dataset.r); if (isN) inp.inputMode = 'decimal'; c.textContent = ''; c.appendChild(inp); inp.focus(); if (start === undefined) inp.select(); editing = { c, inp, old, isN }; } function done(save, move, focus) { if (!editing) return; const e = editing; editing = null; const v = save ? e.inp.value.trim() : String(e.old); if (e.isN) { let n = v === '' ? 0 : Number(v.replace(/,/g, '')); const bad = !isFinite(n) || n < 0 || n > 24; if (bad) { n = Number(e.old || 0); if (save) live.textContent = 'Hours must be a number from 0 to 24'; } e.c.dataset.v = n || ''; if (!n) delete e.c.dataset.v; e.c.textContent = n ? hrs(n) : ''; recalc(); } else e.c.textContent = v; const r = +e.c.dataset.r, k = +e.c.dataset.c; select(move === 'down' ? cell(Math.min(BOTTOM, r + 1), k) : move === 'right' ? cell(r, Math.min(LAST, k + 1)) : e.c, focus); } grid.addEventListener('click', (e) => { const c = e.target.closest('td'); if (c && !editing) select(c); }); grid.addEventListener('dblclick', (e) => { const c = e.target.closest('td'); if (c) { select(c); edit(c); } }); grid.addEventListener('keydown', (e) => { if (editing) { if (e.key === 'Enter') { e.preventDefault(); done(true, 'down'); } else if (e.key === 'Tab') { e.preventDefault(); done(true, 'right'); } else if (e.key === 'Escape') { e.preventDefault(); done(false); } return; } const r = +cur.dataset.r, k = +cur.dataset.c; const to = { ArrowUp: [r - 1, k], ArrowDown: [r + 1, k], ArrowLeft: [r, k - 1], ArrowRight: [r, k + 1], Home: [r, 0], End: [r, LAST] }[e.key]; if (to) { e.preventDefault(); select(cell(Math.max(1, Math.min(BOTTOM, to[0])), Math.max(0, Math.min(LAST, to[1])))); return; } if (e.key === 'Enter' || e.key === 'F2') { e.preventDefault(); edit(cur); return; } if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); edit(cur, e.key); } }); grid.addEventListener('focusout', (e) => { if (editing && e.target === editing.inp) done(true, undefined, false); }); select(cur, false); EOF node .claude/skills/spreadsheet-grid/scripts/check.mjs index.html --out check 2>&1 | tail -30No. Spreadsheet Grid 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.
Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode.
A ZIP of the whole skill folder (34 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.
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 ./spreadsheet-grid with your agent's flag.
One HTML file with its CSS inside: no build step, no framework. An editable spreadsheet-style data grid. Checked at phone and desktop widths.
Node 20+ and Chrome, and no API key. Each use costs nothing beyond your agent.
7 files, about 22,957 tokens in all. The SKILL.md is about 2,206 tokens, and your agent reads it only when the job fits.
Our 1 runs took 1 min 43 s to 1 min 43 s and $0.32 to $0.32 of API use each, in Claude Code with claude-sonnet-5-5. On a Claude plan, a run counts toward your usage instead.
Type /spreadsheet-grid first to make sure it loads. Without it, Claude Code may judge a small job simple enough to do without the skill.