The same 9 ACTRIS parcels from the parent rubric tool, presented as a focused editable + sortable table with a sticky top row. Click any column header to sort; click any cell to edit. Edits save to your browser. The composite Score is the rubric-weighted average and updates as you change any of the 12 score columns. The Table Component Pattern below documents the allowable column parameters and the rule for building new tables like this one across the network.
A single-file, dependency-free editable + sortable HTML table that runs entirely in the browser, persists row data to localStorage, and follows the same paper-and-ink design system used across land.wholetech.com, realhotsprings.com, and the rest of the network.
Every table is built from two arrays:
COLUMNS — the schema (what columns, what types)DATA — the seed rows shipped with the pageThe renderer reads COLUMNS to draw the header (with sort affordance) and reads DATA (or localStorage, if present) to draw the body. Every input/select writes back to the row object and persists. No frameworks, no build step, no fetch — just open the file.
When you build a new table page on the network, you copy this file, replace the two arrays at the top of the <script> block, and ship. Don't introduce a framework, don't add a server round-trip, don't change the design language. If you need a column type that isn't in the spec at right, add it to the spec first, then implement it once, here — so the next table inherits it.
<input type=text>. Sort: alphabetic, case-insensitive. Used for addresses, names, notes.<input type=number>. Sort: numeric. Used for acres, counts, sizes. Optional min / max / step.$1.86M / $295K only in derived columns. Sort: numeric. Treats 0 as "no price".options:[…]. <select>. Sort: by option order, not alphabetic. Used for water (yes/no/tbd), county, status flags.<select>. Sort: numeric. Feeds into computed roll-ups.compute(row) function on the column. Re-runs every render. Used for Score (weighted avg) and Verdict (GO / MAYBE / PASS).<a> rendered next to the input. Sort: alphabetic on the underlying string.COLUMNS declares schema, DATA declares seed rows. Change those, redeploy.thead th { position:sticky; top:0; z-index:5 }. The wrap is the scroll container, not the page.input / change events both write through.localStorage[STORAGE_KEY], namespaced per-page (e.g. land_listings_v1). Bump the version when the schema changes.DATA. Confirm before destroying edits.// New table? Copy /listings/index.html, replace the two arrays: const COLUMNS = [ { key:"addr", label:"Address", type:"text", width:"col-addr" }, { key:"acres", label:"Acres", type:"number", step:0.01, width:"col-num" }, { key:"price", label:"Price", type:"currency", width:"col-price" }, { key:"water", label:"H₂O", type:"select", options:["yes","no","tbd"], width:"col-water", css:"water-col" }, { key:"c0", label:"C1 Zon", type:"score", width:"col-score" }, { key:"score", label:"Score", type:"computed", width:"col-scoresum", compute:r => weightedAvg(r), tone:scoreTone }, { key:"_del", label:"", type:"action", action:"delete", width:"col-del" }, ]; const DATA = [ { addr:"…", acres:10.7, price:315000, water:"yes", c0:4, … }, … ]; const STORAGE_KEY = "land_listings_v1";