openvibers/openvibe.work Commits

The home's picture is the newest listing, leading with its board (#250)

OP OpenVibers committed 3 days ago · 6ad4b0cf8a · 4 files, +99 −14

* The home's picture is the newest listing, leading with its board

The hero drew the shared orbit. It now shows the thing itself: the newest remote listing as a card whose first line is
the board it came from, on a small stack of the next two. Real rows only: with no listings there is no picture.

* Size budget: the home's picture, measured

Changes

ddiff --git a/public/css/home.css b/public/css/home.css
iindex 76e2413..76dc0ab 100644
---- a/public/css/home.css
++++ b/public/css/home.css
@@@ -95,3 +95,29 @@
.fold-bare { margin-top: 0; }
.fold:not(.fold-bare) .fold-body > .sc-sec { margin-top: 0; }
}
++
++/* ── The home's picture (server/http/pages.js sourceScene): the newest listing, leading with its source, on a small
++ stack of the next two. The cards behind show only their top edge: enough to read the board and the title. ── */
++.wk-scene { width: min(100%, 400px); margin: 0 auto; }
++.wk-scene-stack { display: grid; padding-top: 84px; }
++.wk-scene-card { grid-area: 1 / 1; display: grid; gap: 4px; min-width: 0; padding: 14px 16px; border-radius: 16px; border: 1px solid var(--border, #1f2d47); color: var(--text-primary, #e6edf7); text-decoration: none; box-shadow: 0 12px 30px rgba(0, 0, 0, .28);
++ /* Solid whatever the theme's card colour is (some are see-through): a card must cover the one behind it. */
++ background: linear-gradient(var(--bg-card, #131c2e), var(--bg-card, #131c2e)), var(--bg-primary, #0a0f1c); }
++.wk-scene-card b { font-size: 1.05rem; line-height: 1.25; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
++.wk-scene-card.is-second { transform: translateY(-42px) scale(.95); transform-origin: top center; }
++.wk-scene-card.is-second > *, .wk-scene-card.is-third > * { opacity: .7; }
++.wk-scene-card.is-third { transform: translateY(-84px) scale(.9); transform-origin: top center; }
++.wk-scene-card.is-third > * { opacity: .45; }
++.wk-scene-card.is-second b, .wk-scene-card.is-third b { -webkit-line-clamp: 1; }
++.wk-scene-card.is-front:hover, .wk-scene-card.is-front:focus-visible { border-color: var(--accent, #3b82f6); outline: none; }
++.wk-scene-src { justify-self: start; display: inline-flex; align-items: center; gap: 7px; padding: 3px 10px 3px 8px; border-radius: 999px; font-size: .78rem; font-weight: 750; color: var(--accent-light, #60a5fa); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }
++.wk-scene-src i { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
++.is-front .wk-scene-src { animation: wkSceneRing 1.4s var(--ov-ease-out, cubic-bezier(.2, .8, .2, 1)) .7s 1 both; }
++.wk-scene-meta { color: var(--text-secondary, #96a7c2); font-size: .9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
++.wk-scene-go { margin-top: 6px; color: var(--accent-light, #60a5fa); font-size: .86rem; font-weight: 650; }
++.wk-scene figcaption { margin-top: 10px; text-align: center; color: var(--text-muted, #7386a3); font-size: .8rem; }
++@keyframes wkSceneRing { 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent, #3b82f6) 70%, transparent); } 100% { box-shadow: 0 0 0 12px transparent; } }
++@media (prefers-reduced-motion: reduce) { .is-front .wk-scene-src { animation: none; } }
++html[data-ov-motion=reduced] .is-front .wk-scene-src { animation: none; }
++/* A transformed card paints above a plain one whatever the order in the page: the front card says it is in front. */
++.wk-scene-card.is-front { position: relative; z-index: 2; }
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 69810a4..b510b26 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -133,6 +133,28 @@ ${facets.type.map((t) => link({ type: t.type }, t.label, filters.type === t.type
const FAQ = ((products.get('openvibe.work') || {}).faq || []);
const n = (v) => Number(v || 0).toLocaleString('en-US');
const sentence = (t) => (t ? t[0].toUpperCase() + t.slice(1) : t);
++ /**
++ * The home's picture: the newest listing as a card that leads with its source ("From Remotive"), the way every
++ * listing on the site does, with the next two showing behind it. Every word in it is a real row; the caption says
++ * when it was read. The source tag is ringed once when the page opens (CSS, public/css/home.css .wk-scene), and
++ * is simply there under reduced motion.
++ */
++ function sourceScene(rows, sourceOf) {
++ const tile = (row, cls) => {
++ const l = listing.meta(row);
++ const src = sourceOf(row) || {};
++ return html`<a class="wk-scene-card ${cls}" href="/jobs/${l.id}"${cls === 'is-front' ? '' : raw(' tabindex="-1" aria-hidden="true"')}>
++<span class="wk-scene-src"><i aria-hidden="true"></i>From ${src.name || 'a public board'}</span>
++<b>${l.title}</b>
++<span class="wk-scene-meta">${l.company}${l.location ? html` · ${l.location}` : ''}</span>
++${cls === 'is-front' ? html`<span class="wk-scene-go">Read it, with the link to the original</span>` : ''}</a>`;
++ };
++ const [first, ...rest] = rows.slice(0, 3);
++ return html`<figure class="wk-scene" aria-label="The newest remote listing, with the board it came from">
++<div class="wk-scene-stack">${rest.reverse().map((row, i, all) => tile(row, all.length - i === 2 ? 'is-third' : 'is-second'))}${tile(first, 'is-front')}</div>
++<figcaption>The newest remote listing right now. Every listing names its board.</figcaption></figure>`;
++ }
++
r.get('/', async (req, res) => {
const [latest, counts, sum, salaried] = await Promise.all([store.listRecent(s, 6, { remote: true }), store.countsBySource(s), store.homeSummary(s), store.listSalaried(s, 4)]);
const fetches = await store.sourceFetches(s);
@@@ -142,19 +164,10 @@ ${facets.type.map((t) => link({ type: t.type }, t.label, filters.type === t.type
// The title says what this is; the eyebrow is the site's name, not the title again.
eyebrow: SITE_NAME,
title: 'Find work.',
-- // The hero's picture is this site's own (2026-11-01): it drew the generic aura, the same on six homes.
-- // What a job seeker has here, around the listings.
-- orbit: {
-- label: 'What you can do here',
-- center: { title: 'Listings', sub: 'from public boards' },
-- inner: [
-- { icon: 'ov:search', label: 'Search', href: '#search' },
-- { icon: 'ov:map', label: 'Remote', href: '/jobs?remote=true' },
-- { icon: 'ov:bell', label: 'Alerts', href: '#search' },
-- { icon: 'ov:actor', label: 'Your agent', href: 'https://openvibe.actor/' },
-- { icon: 'ov:link', label: 'Sources', href: '/sources' },
-- ],
-- },
++ // The hero's picture is the thing itself (2026-11-10; the owner: no more "generic circled" homes): the
++ // newest listing, as a card whose first line is the board it came from, on a small stack of the next two.
++ // Real rows only: with no listings yet there is no picture.
++ ...(latest.length ? { aside: { html: String(sourceScene(latest, sourceOf)) } } : {}),
accent: 'Every listing shows where it came from.',
// Two lines on a phone: what it is and that it is free. What signing in adds is said where it happens.
lede: 'Listings from public job boards, each with its own link. No paywall, and no account needed to look.',
ddiff --git a/test/home-picture.test.js b/test/home-picture.test.js
nnew file mode 100644
iindex 0000000..dfd98f9
---- /dev/null
++++ b/test/home-picture.test.js
@@@ -0,0 +1,46 @@
++'use strict';
++// The home's picture is the thing itself: the newest remote listing as a card that leads with the board it came from,
++// on a small stack of the next two (the owner, 2026-11-09: no more "generic circled" homes). Real rows only: with no
++// listings there is no picture, and never an invented one.
++const assert = require('assert');
++const { boot, check, done } = require('./helpers/boot');
++
++(async () => {
++ const t = await boot();
++
++ await check('with no listings the hero has no picture (and no orbit)', async () => {
++ const r = await t.get('/');
++ assert.strictEqual(r.status, 200);
++ assert.ok(!r.text.includes('class="wk-scene"'), 'no picture without a listing');
++ assert.ok(!/class="sc-orbit|sc-orbit-/.test(r.text), 'the generic orbit is gone');
++ });
++
++ const rows = [
++ { title: 'Backend engineer', company: 'Nordlicht Systems', location: 'Berlin, Germany', at: '2026-11-09T10:00:00.000Z' },
++ { title: 'Data scientist', company: 'Helles Werk', location: 'Remote, Europe', at: '2026-11-09T11:00:00.000Z' },
++ { title: 'Support lead', company: 'Kleine Post', location: '', at: '2026-11-09T12:00:00.000Z' },
++ ];
++ await Promise.all(rows.map((row, i) => t.ctx.s.db.query(
++ `INSERT INTO work_listings (id, source, source_id, url, title, company, location, remote, tags, job_type, salary, excerpt, search_text, posted_at, sort_at, fetched_at, first_seen_at, last_seen_at)
++ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$14,$14,$14,$14)`,
++ [`job_pic${'0'.repeat(22)}${i}`, 'arbeitnow', `pic-${i}`, `https://example.org/job/${i}`, row.title, row.company, row.location,
++ true, 'nodejs', 'permanent', '', 'A short excerpt of the listing.', `${row.title} ${row.company}`.toLowerCase(), row.at])));
++
++ await check('with listings the picture is the newest one, leading with its board, over the next two', async () => {
++ const r = await t.get('/');
++ const scene = (r.text.match(/<figure class="wk-scene"[\s\S]*?<\/figure>/) || [''])[0];
++ assert.ok(scene, 'the picture is in the hero');
++ const cards = Array.from(scene.matchAll(/<a class="wk-scene-card (is-front|is-second|is-third)" href="(\/jobs\/[^"]+)"([^>]*)>\s*<span class="wk-scene-src"><i aria-hidden="true"><\/i>From ([^<]+)<\/span>\s*<b>([^<]+)<\/b>/g));
++ assert.deepStrictEqual(cards.map((m) => m[1]), ['is-third', 'is-second', 'is-front'], 'the front card is last in the markup, so it is on top');
++ const front = cards[2];
++ assert.strictEqual(front[5], 'Support lead', 'the front card is the newest listing');
++ assert.ok(front[4].length > 2 && front[4] !== 'a public board', `it names its board: ${front[4]}`);
++ assert.ok(!/aria-hidden/.test(front[3]), 'the front card is a real link');
++ cards.slice(0, 2).forEach((m) => assert.ok(/tabindex="-1" aria-hidden="true"/.test(m[3]), 'the cards behind are decoration: out of the tab order'));
++ assert.match(scene, /<figcaption>The newest remote listing right now\. Every listing names its board\.<\/figcaption>/);
++ assert.ok(!/class="sc-orbit|sc-orbit-/.test(r.text), 'the generic orbit is gone');
++ });
++
++ await t.close();
++ done();
++})().catch((e) => { console.error(e); process.exit(1); });
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
iindex 0199eaf..4061a3c 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -24,7 +24,7 @@ const BUDGETS = {
jsBrotliKB: 89.5, // 89.5: measured 87.5 with openvibe-shared 4.163.0, plus room for 4.162.0–4.163.0, 2026-11-09
cssFiles: 3, // 3 (app.css, the cached /shared/showcase.css and css/home.css, the curated home's counts, fields, cities, companies and questions, 2026-10-22)
cssRawKB: 53.7, // 53.7: measured 48.7 with openvibe-shared 4.161.0, plus room for 4.162.0–4.163.0, 2026-11-09
-- cssBrotliKB: 11, // 10.4 with 2026-11-01: phone table cards (+1.3 KB raw) (2026-11-01); raised from 10 on 2026-10-31, with the measured page: 45.6 KB raw / 10.1 KB brotli (the listings' salary line, the gone line and the fetch date under the link out); 10.0 with the home's pick block and folds (the sections below the search are picked one at a time on a phone, 2026-10-30); 9.1 with openvibe-shared 4.48.0 (the site switcher on the mark, the menu's Start tab and quick tools; Kit's touch hit areas and the phone navbar fix, 4.44-4.47.3); 8.6 the home's phone rows, limits and fold (2026-10-29); 6.6–6.7 with Shared 4.13.0; raised from 8 for openvibe-shared v4.39.0 (+1.5 KB raw of kit/room/showcase CSS); measured 8.3
++ cssBrotliKB: 11.4, // 11.3 with the home's picture (the newest listing as a stack of cards), 2026-11-10; before: 10.4 with 2026-11-01: phone table cards (+1.3 KB raw) (2026-11-01); raised from 10 on 2026-10-31, with the measured page: 45.6 KB raw / 10.1
externalFiles: 0, // 0
};