# Tabla de costeo / Tabla de costeo

> Edición, selección, filtros y totales de partidas.

Page: https://componentes-lovat.vercel.app/c/cost-table/  
License: MIT · Design:   
Requires: tokens.css  
Context: herramienta · Density: media · Tone: sobria

## Files

- Tokens (once): https://componentes-lovat.vercel.app/tokens.css
- HTML: https://componentes-lovat.vercel.app/raw/cost-table/cost-table.html
- CSS: https://componentes-lovat.vercel.app/raw/cost-table/cost-table.css
- JS: https://componentes-lovat.vercel.app/raw/cost-table/cost-table.js
- Everything in one file: https://componentes-lovat.vercel.app/r/cost-table.html

Dark theme: add class `dark` to `<html>`.

## When to use

- Edición, selección, filtros y totales de partidas.

## When not to use

- Examples do not connect accounts or save files to a server.

## Accessibility

Roles: region

| Keys | Action |
|---|---|
| Tab / Enter | Navigate and activate controls |

- Errors and results use role=status.

## Code

```html
<section class="work-card" aria-label="Costeo"><h2>Costeo de materiales</h2><p class="muted">Edita cantidades y precios. Importes de ejemplo en USD.</p><label>Filtrar partidas<input data-filter type="search" placeholder="Material o concepto"></label><div class="table-scroll"><table><thead><tr><th>Seleccionar</th><th>Concepto</th><th>Cantidad</th><th>Precio USD</th><th>Subtotal</th></tr></thead><tbody data-rows></tbody></table></div><div class="actions"><button data-add>Añadir partida</button><button data-remove>Quitar seleccionadas</button></div><p>Total USD: <output data-total></output></p><p role="status" data-status></p></section>
```

```css
.work-card{width:min(720px,100%);padding:24px;box-sizing:border-box;border:1px solid var(--border);border-radius:12px;background:var(--background);color:var(--foreground);font:14px/1.5 var(--font-sans)}.work-card h2{font-size:24px;margin:0 0 8px}.work-card label{display:grid;gap:5px;margin:12px 0}.work-card input,.work-card textarea,.work-card button{font:inherit;border:1px solid var(--border);border-radius:6px;padding:8px;background:var(--background);color:var(--foreground)}.work-card button{cursor:pointer}.work-card button:disabled{opacity:.45}.work-card :focus-visible{outline:2px solid var(--foreground);outline-offset:2px}.work-card table{width:100%;border-collapse:collapse}.work-card td,.work-card th{padding:8px 4px;text-align:left;border-bottom:1px solid var(--border)}.work-card td input{width:100%;box-sizing:border-box;min-width:65px}.work-card .table-scroll{overflow:auto}.work-card .table-scroll table{min-width:460px}.work-card .actions{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0}.work-card [hidden]{display:none!important}.work-card pre{white-space:pre-wrap;max-height:240px;overflow:auto}.work-card .steps{color:var(--muted-foreground);font-size:12px}.work-card img{max-width:100%;max-height:200px;object-fit:contain}.work-card output{font-weight:600}.work-card .muted{color:var(--muted-foreground)}
```

```js
(()=>{document.querySelectorAll('[aria-label="Costeo"]').forEach(root=>{let rows=[{name:'Material',qty:2,price:45},{name:'Transporte',qty:1,price:20}];const body=root.querySelector('[data-rows]'), money=n=>n.toFixed(2);function totals(){let total=0;body.querySelectorAll('tr').forEach((tr,i)=>{const vals=tr.querySelectorAll('input');rows[i]={name:vals[1].value,qty:Number(vals[2].value),price:Number(vals[3].value)};const sub=rows[i].qty*rows[i].price;tr.querySelector('output').textContent=money(sub);total+=sub;});root.querySelector('[data-total]').textContent=money(total);}function render(){body.replaceChildren();rows.forEach(r=>{const tr=document.createElement('tr');tr.innerHTML='<td><input type="checkbox" aria-label="Seleccionar partida"></td><td><input aria-label="Concepto"></td><td><input type="number" min="0" step="0.01" aria-label="Cantidad"></td><td><input type="number" min="0" step="0.01" aria-label="Precio"></td><td><output></output></td>';const inputs=tr.querySelectorAll('input');inputs[1].value=r.name;inputs[2].value=r.qty;inputs[3].value=r.price;body.append(tr);});totals();}body.addEventListener('input',()=>{if([...body.querySelectorAll('input[type=number]')].some(i=>!i.validity.valid)){root.querySelector('[data-status]').textContent='Revisa cantidades y precios: usa valores no negativos.';return;}root.querySelector('[data-status]').textContent='';totals();});root.querySelector('[data-add]').onclick=()=>{rows.push({name:'Nueva partida',qty:1,price:0});render();};root.querySelector('[data-remove]').onclick=()=>{rows=rows.filter((r,i)=>!body.rows[i].querySelector('input').checked);render();};root.querySelector('[data-filter]').oninput=e=>{[...body.rows].forEach((tr,i)=>tr.hidden=!rows[i].name.toLowerCase().includes(e.target.value.toLowerCase()));};root.exportRows=()=>rows.map(r=>({...r}));render();});})();
```

## Credits

