openvibers/openvibe.rent Commits

City tables name their columns on a phone (#178)

OP OpenVibers committed 12 days ago · dc5b385f8e · 3 files, +22 −14

Changes

ddiff --git a/public/css/app.css b/public/css/app.css
iindex d4f3519..8a0a9ef 100644
---- a/public/css/app.css
++++ b/public/css/app.css
@@@ -93,19 +93,20 @@ button.sc-btn.sc-primary { border: 0; }
.ew-chips { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; padding: 0; margin: 12px 0 6px; }
.ew-chips li { padding: 5px 11px; border-radius: 999px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, #131c2e); font-size: .9rem; }
--/* Tables on a phone (phone sweep, 2026-11-01): a table of three columns or more shows each row as a card, the first
++/* Tables on a phone (phone sweep, 2026-11-01): a table of three columns or more whose cells name their column
++ (data-label) shows each row as a card, the first
cell its title and every other cell named by its column (html.js gives each cell its data-label). Narrower tables
stay tables. Nothing is left out and nothing scrolls sideways. */
@media (max-width: 640px) {
-- .table-wrap:has(> table thead th:nth-child(3)) { overflow: visible; }
-- .table-wrap > table:has(thead th:nth-child(3)) { display: block; min-width: 0; }
-- .table-wrap > table:has(thead th:nth-child(3)) thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
-- .table-wrap > table:has(thead th:nth-child(3)) tbody { display: grid; gap: 10px; }
-- .table-wrap > table:has(thead th:nth-child(3)) tr { display: grid; gap: 8px; padding: 12px 14px; border: 1px solid var(--border, #1f2d47); border-radius: 14px; background: var(--bg-card, var(--bg-secondary, #101828)); }
-- .table-wrap > table:has(thead th:nth-child(3)) td { display: block; min-width: 0; padding: 0; border: 0; overflow-wrap: anywhere; }
-- .table-wrap > table:has(thead th:nth-child(3)) td:first-child { font-weight: 700; color: var(--text-primary, #e6edf7); }
-- .table-wrap > table:has(thead th:nth-child(3)) td[data-label]:not(:first-child)::before { content: attr(data-label); display: block; margin-bottom: 2px; font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted, #7386a3); }
-- .table-wrap > table:has(thead th:nth-child(3)) td:empty { display: none; }
++ .table-wrap:has(> table thead th:nth-child(3)):has(> table td[data-label]) { overflow: visible; }
++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) { display: block; min-width: 0; }
++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) tbody { display: grid; gap: 10px; }
++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) tr { display: grid; gap: 8px; padding: 12px 14px; border: 1px solid var(--border, #1f2d47); border-radius: 14px; background: var(--bg-card, var(--bg-secondary, #101828)); }
++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) td { display: block; min-width: 0; padding: 0; border: 0; overflow-wrap: anywhere; }
++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) td:first-child { font-weight: 700; color: var(--text-primary, #e6edf7); }
++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) td[data-label]:not(:first-child)::before { content: attr(data-label); display: block; margin-bottom: 2px; font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted, #7386a3); }
++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) td:empty { display: none; }
}
/* A sign-in prompt: the sentence, then the button under it (2026-11-01). */
.notice.signin-prompt { display: grid; gap: 12px; justify-items: start; }
@@@ -113,3 +114,10 @@ button.sc-btn.sc-primary { border: 0; }
/* The rent trend line is drawn 320 wide; on a 320 px phone its box overran the gutter (2026-11-01). */
svg.rent-trend { max-width: 100%; height: auto; }
++@media (max-width: 640px) {
++ /* A row's own header (th scope=row) is the card's title, like a first cell. */
++ .table-wrap > table:has(thead th:nth-child(3)):has(td[data-label]) tbody th { display: block; padding: 0; border: 0; text-align: left; font-weight: 700; }
++ /* The city tables are all figures: two to a row, the city across the top. */
++ .table-wrap > table.city-table:has(td[data-label]) tr { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
++ .table-wrap > table.city-table:has(td[data-label]) tbody th { grid-column: 1 / -1; }
++}
ddiff --git a/server/homes/city-pages.js b/server/homes/city-pages.js
iindex 90a1b47..9d7517d 100644
---- a/server/homes/city-pages.js
++++ b/server/homes/city-pages.js
@@@ -52,7 +52,7 @@ function mountHomesCityPages(r, { config, s, page }) {
<section class="sc-sec" aria-labelledby="h-big"><h2 id="h-big">The largest cities</h2>
<div class="table-wrap"><table class="city-table"><caption class="sr-only">Median home value and typical 2-bedroom rent in the 30 largest US cities</caption>
<thead><tr><th scope="col">City</th><th scope="col">Median home value</th><th scope="col">2-bedroom rent</th></tr></thead>
--<tbody>${largest.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}, ${c.state}</a></th><td>${valueCell(c)}</td><td>${cities.rentText(c.rent.b2) || html`<span class="muted">not published</span>`}</td></tr>`)}</tbody></table></div>
++<tbody>${largest.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}, ${c.state}</a></th><td data-label="Median home value">${valueCell(c)}</td><td data-label="2-bedroom rent">${cities.rentText(c.rent.b2) || html`<span class="muted">not published</span>`}</td></tr>`)}</tbody></table></div>
${source()}</section>`,
});
});
@@@ -72,7 +72,7 @@ ${source()}</section>`,
<p class="sc-lede">${list.length === 1 ? html`${list[0].name} is ${name}'s largest city` : html`${list.length} cities in ${name}, largest first`}: what homes are worth, what renting costs, and a page for each with every for-sale site's search.</p>
<div class="table-wrap"><table class="city-table"><caption class="sr-only">Median home value and typical rent by city in ${name}</caption>
<thead><tr><th scope="col">City</th><th scope="col">Median home value</th><th scope="col">2-bedroom rent</th></tr></thead>
--<tbody>${list.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}</a></th><td>${valueCell(c)}</td><td>${cities.rentText(c.rent.b2) || html`<span class="muted">not published</span>`}</td></tr>`)}</tbody></table></div>
++<tbody>${list.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}</a></th><td data-label="Median home value">${valueCell(c)}</td><td data-label="2-bedroom rent">${cities.rentText(c.rent.b2) || html`<span class="muted">not published</span>`}</td></tr>`)}</tbody></table></div>
${source()}
<p><a href="/in">Every state</a> · <a href="/records">Property records</a> · <a href="/mortgage">Mortgage calculator</a></p>`,
});
ddiff --git a/server/http/city-pages.js b/server/http/city-pages.js
iindex 800faf5..f79f6a9 100644
---- a/server/http/city-pages.js
++++ b/server/http/city-pages.js
@@@ -102,7 +102,7 @@ ${city ? html`<p class="small muted fair-how">Within the Census' margin of the m
<section class="sc-sec" aria-labelledby="h-big"><h2 id="h-big">The largest cities</h2>
<div class="table-wrap"><table class="city-table"><caption class="sr-only">Typical rents in the 30 largest US cities</caption>
<thead><tr><th scope="col">City</th><th scope="col">1 bedroom</th><th scope="col">2 bedrooms</th><th scope="col">3 bedrooms</th></tr></thead>
--<tbody>${largest.map((c) => html`<tr><th scope="row"><a href="${c.path}">${placeLabel(c)}</a></th><td>${cell(c, 'b1')}</td><td>${cell(c, 'b2')}</td><td>${cell(c, 'b3')}</td></tr>`)}</tbody></table></div>
++<tbody>${largest.map((c) => html`<tr><th scope="row"><a href="${c.path}">${placeLabel(c)}</a></th><td data-label="1 bedroom">${cell(c, 'b1')}</td><td data-label="2 bedrooms">${cell(c, 'b2')}</td><td data-label="3 bedrooms">${cell(c, 'b3')}</td></tr>`)}</tbody></table></div>
${source()}</section>${cpiBlock({ national, metro: null })}`,
});
});
@@@ -123,7 +123,7 @@ ${source()}</section>${cpiBlock({ national, metro: null })}`,
<p class="sc-lede">${list.length === 1 ? html`${list[0].name} is ${name}'s largest city` : html`${list.length} cities in ${name}, largest first`}: what rent usually costs, and a page for each with every rental site's search for it.</p>
<div class="table-wrap"><table class="city-table"><caption class="sr-only">Typical rents by city in ${name}</caption>
<thead><tr><th scope="col">City</th><th scope="col">Studio</th><th scope="col">1 bedroom</th><th scope="col">2 bedrooms</th><th scope="col">3 bedrooms</th></tr></thead>
--<tbody>${list.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}</a></th><td>${cell(c, 'studio')}</td><td>${cell(c, 'b1')}</td><td>${cell(c, 'b2')}</td><td>${cell(c, 'b3')}</td></tr>`)}</tbody></table></div>
++<tbody>${list.map((c) => html`<tr><th scope="row"><a href="${c.path}">${c.name}</a></th><td data-label="Studio">${cell(c, 'studio')}</td><td data-label="1 bedroom">${cell(c, 'b1')}</td><td data-label="2 bedrooms">${cell(c, 'b2')}</td><td data-label="3 bedrooms">${cell(c, 'b3')}</td></tr>`)}</tbody></table></div>
${source()}
<p><a href="/in">Every state</a> · <a href="/fair">Check a rent</a></p>`,
});