openvibers/openvibe.work Commits
Work: listings as cards with a company tile, chips for remote, type, pay and tags, and a two-line excerpt
Changes
@@@ -133,7 +133,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md).Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).<!-- versions:start -->--- openvibe-contracts: v0.115.0++- openvibe-contracts: v0.116.0- openvibe-sdk: v0.35.2--- openvibe-shared: v2.14.0++- openvibe-shared: v2.15.0<!-- versions:end -->@@@ -1,12 +1,12 @@{"name": "openvibe-work",-- "version": "0.1.0",++ "version": "0.2.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-work",-- "version": "0.1.0",++ "version": "0.2.0","license": "AGPL-3.0-only","dependencies": {"cookie-parser": "^1.4.7",@@@ -15,9 +15,9 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.115.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.116.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.14.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0","pg": "^8.23.0"},"devDependencies": {@@@ -871,9 +871,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.115.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.115.0",-- "integrity": "sha512-2si6od4+RLgZEIAuWMmTrLZL65TzvCwB/sKoe24vSzC9+sNPt2RXWGXDCwE2fB90vmaRvX6pI+DQyu18QN2f0Q==",++ "version": "0.116.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.116.0",++ "integrity": "sha512-mDqQJ6m8YSJf3UV+kVpqeMrgNDMMzBQdYmj/3o5JqGegj52ymiDH1NerVW6c+0ebsV3zX7RbRlknIy3vr634yw==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -916,9 +916,9 @@}},"node_modules/openvibe-shared": {-- "version": "2.14.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.14.0",-- "integrity": "sha512-XraQxTERs14Jc65sJ23Hv0B5BJLC3Fbwsg3uEp8rMaEShFiuhMofr7pKGYX11sgreMMF64tymO6l6kdjYTBJfw==",++ "version": "2.15.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0",++ "integrity": "sha512-xApTyiUBVDQLTdnKEjjr/9uUU4+T5gofz9DqXv0MrLVINmW88uwbfoEht3kAlG9mp1792U6kIFEblJDHn2wyMQ==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -23,9 +23,9 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.115.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.116.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.14.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0","pg": "^8.23.0"},"engines": {@@@ -55,17 +55,26 @@ button.sc-btn.sc-primary { border: 0; }.facets li.on a { border-color: var(--accent, #3b82f6); color: var(--text-primary, #e6edf7); }.facet-n { color: var(--text-muted, #7386a3); }--.job-list { list-style: none; margin: 8px 0 0; padding: 0; }--.job-item { border-top: 1px solid var(--border, #1f2d47); padding: 14px 0; }--.job-item:first-child { border-top: 0; }--.job-title { margin: 0 0 4px; }--.job-meta { margin: 0 0 6px; color: var(--text-secondary, #96a7c2); font-size: .92rem; }--.job-company { font-weight: 600; color: var(--text-primary, #e6edf7); }--.job-excerpt { margin: 0 0 8px; max-width: 78ch; }--.job-tags { list-style: none; display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 8px; padding: 0; }--.job-tags li { font-size: .78rem; border: 1px solid var(--border-light, #2c3d5c); border-radius: 999px; padding: 0 8px; color: var(--text-muted, #7386a3); }--.job-provenance { margin: 0; font-size: .88rem; }++.job-list { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 10px; }++.job-item { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 14px; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, #131c2e); transition: border-color .15s, transform .15s; }++.job-item:hover { border-color: color-mix(in srgb, var(--accent, #3b82f6) 60%, var(--border, #1f2d47)); transform: translateY(-1px); }++.job-logo { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; font-weight: 800; font-size: 1.1rem; color: hsl(var(--h) 80% 78%); background: hsl(var(--h) 45% 22%); border: 1px solid hsl(var(--h) 40% 32%); }++.job-main { min-width: 0; }++.job-title { margin: 0 0 3px; font-size: 1.05rem; line-height: 1.3; }++.job-title a { color: var(--text-primary, #e6edf7); text-decoration: none; }++.job-title a:hover { color: var(--accent-light, #60a5fa); text-decoration: underline; text-underline-offset: 3px; }++.job-meta { margin: 0 0 8px; display: flex; flex-wrap: wrap; gap: 2px 10px; font-size: .9rem; color: var(--text-secondary, #96a7c2); }++.job-company { font-weight: 650; color: var(--text-primary, #e6edf7); }++.job-chips { margin: 0 0 8px; display: flex; flex-wrap: wrap; gap: 6px; }++.chip { font-size: .76rem; font-weight: 600; padding: 2px 9px; border-radius: 999px; border: 1px solid var(--border-light, #2c3d5c); color: var(--text-secondary, #96a7c2); white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }++.chip-ok { color: var(--success, #22c55e); border-color: color-mix(in srgb, var(--success, #22c55e) 45%, transparent); }++.chip-pay { color: var(--warning, #f59e0b); border-color: color-mix(in srgb, var(--warning, #f59e0b) 45%, transparent); }++.chip-tag { color: var(--text-muted, #7386a3); font-weight: 500; }++.job-excerpt { margin: 0 0 8px; max-width: 80ch; font-size: .92rem; line-height: 1.5; color: var(--text-secondary, #96a7c2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }++.job-provenance { margin: 0; font-size: .82rem; }.job-source { font-weight: 600; }++@media (max-width: 560px) { .job-item { grid-template-columns: 1fr; } .job-logo { display: none; } }++@media (prefers-reduced-motion: reduce) { .job-item { transition: none; } }.job-pager { display: flex; gap: 12px; align-items: center; margin: 18px 0 8px; }.job-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 16px; margin: 16px 0; font-size: .92rem; }.job-facts dt { color: var(--text-secondary, #96a7c2); }@@@ -47,15 +47,26 @@ function provenance(row, source) {return html`<p class="job-provenance"><span class="muted">Listing from</span> ${sourceLink(row, source)}${row.posted_at ? html` <span class="muted">· posted ${dateWords(row.posted_at)}</span>` : ''}</p>`;}--/** One listing in a list: title, who and where, the excerpt, the tags, and the link back to the board. */++/** A company's initial on a tint picked from its name: the same company always gets the same tile. */++function initialTile(company) {++ const name = String(company || '?').trim();++ let h = 0;++ for (const ch of name) h = (h * 31 + ch.codePointAt(0)) % 360;++ return html`<span class="job-logo" style="--h:${h}" aria-hidden="true">${(name[0] || '?').toUpperCase()}</span>`;++}++++/** One listing in a list, as a card: who, what and where at a glance, two lines of the excerpt, and the link back. */function listingItem(row, { source }) {const l = meta(row);return html`<li class="job-item" id="${l.id}">++${initialTile(l.company)}++<div class="job-main"><h3 class="job-title"><a href="/jobs/${l.id}">${l.title}</a></h3>--<p class="job-meta"><span class="job-company">${l.company}</span> · <span class="job-location">${l.location || 'Location not given'}</span>${l.remote ? html` · ${badge('Remote', 'ok')}` : ''}${l.job_type ? html` · <span class="job-type">${l.job_type}</span>` : ''}${l.salary ? html` · <span class="job-salary">${l.salary}</span>` : ''}</p>++<p class="job-meta"><span class="job-company">${l.company}</span><span class="job-location">${l.location || 'Location not given'}</span></p>++<p class="job-chips">${l.remote ? html`<span class="chip chip-ok">Remote</span>` : ''}${l.job_type ? html`<span class="chip">${l.job_type}</span>` : ''}${l.salary ? html`<span class="chip chip-pay">${l.salary}</span>` : ''}${l.tags.slice(0, 3).map((t) => html`<span class="chip chip-tag">${t}</span>`)}</p><p class="job-excerpt">${l.excerpt}</p>--${l.tags.length ? html`<ul class="job-tags">${l.tags.slice(0, 8).map((t) => html`<li>${t}</li>`)}</ul>` : ''}${provenance(row, source)}++</div></li>`;}
@@ -133,7 +133,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md).
@@ -133,7 +133,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md).
Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
<!-- versions:start -->
<!-- versions:start -->
-- openvibe-contracts: v0.115.0
+- openvibe-contracts: v0.116.0
- openvibe-sdk: v0.35.2
- openvibe-sdk: v0.35.2
-- openvibe-shared: v2.14.0
+- openvibe-shared: v2.15.0
<!-- versions:end -->
<!-- versions:end -->
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-work",
"name": "openvibe-work",
- "version": "0.1.0",
+ "version": "0.2.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-work",
"name": "openvibe-work",
- "version": "0.1.0",
+ "version": "0.2.0",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"cookie-parser": "^1.4.7",
"cookie-parser": "^1.4.7",
@@ -15,9 +15,9 @@
@@ -15,9 +15,9 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.115.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.116.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.14.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -871,9 +871,9 @@
@@ -871,9 +871,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.115.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.115.0",
- "integrity": "sha512-2si6od4+RLgZEIAuWMmTrLZL65TzvCwB/sKoe24vSzC9+sNPt2RXWGXDCwE2fB90vmaRvX6pI+DQyu18QN2f0Q==",
+ "version": "0.116.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.116.0",
+ "integrity": "sha512-mDqQJ6m8YSJf3UV+kVpqeMrgNDMMzBQdYmj/3o5JqGegj52ymiDH1NerVW6c+0ebsV3zX7RbRlknIy3vr634yw==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -916,9 +916,9 @@
@@ -916,9 +916,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "2.14.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.14.0",
- "integrity": "sha512-XraQxTERs14Jc65sJ23Hv0B5BJLC3Fbwsg3uEp8rMaEShFiuhMofr7pKGYX11sgreMMF64tymO6l6kdjYTBJfw==",
+ "version": "2.15.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0",
+ "integrity": "sha512-xApTyiUBVDQLTdnKEjjr/9uUU4+T5gofz9DqXv0MrLVINmW88uwbfoEht3kAlG9mp1792U6kIFEblJDHn2wyMQ==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -23,9 +23,9 @@
@@ -23,9 +23,9 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.115.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.116.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.14.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
@@ -55,17 +55,26 @@ button.sc-btn.sc-primary { border: 0; }
@@ -55,17 +55,26 @@ button.sc-btn.sc-primary { border: 0; }
.facets li.on a { border-color: var(--accent, #3b82f6); color: var(--text-primary, #e6edf7); }
.facets li.on a { border-color: var(--accent, #3b82f6); color: var(--text-primary, #e6edf7); }
.facet-n { color: var(--text-muted, #7386a3); }
.facet-n { color: var(--text-muted, #7386a3); }
-.job-list { list-style: none; margin: 8px 0 0; padding: 0; }
-.job-item { border-top: 1px solid var(--border, #1f2d47); padding: 14px 0; }
-.job-item:first-child { border-top: 0; }
-.job-title { margin: 0 0 4px; }
-.job-meta { margin: 0 0 6px; color: var(--text-secondary, #96a7c2); font-size: .92rem; }
-.job-company { font-weight: 600; color: var(--text-primary, #e6edf7); }
-.job-excerpt { margin: 0 0 8px; max-width: 78ch; }
-.job-tags { list-style: none; display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 8px; padding: 0; }
-.job-tags li { font-size: .78rem; border: 1px solid var(--border-light, #2c3d5c); border-radius: 999px; padding: 0 8px; color: var(--text-muted, #7386a3); }
-.job-provenance { margin: 0; font-size: .88rem; }
+.job-list { list-style: none; margin: 10px 0 0; padding: 0; display: grid; gap: 10px; }
+.job-item { display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: 14px; padding: 14px 16px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, #131c2e); transition: border-color .15s, transform .15s; }
+.job-item:hover { border-color: color-mix(in srgb, var(--accent, #3b82f6) 60%, var(--border, #1f2d47)); transform: translateY(-1px); }
+.job-logo { width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center; font-weight: 800; font-size: 1.1rem; color: hsl(var(--h) 80% 78%); background: hsl(var(--h) 45% 22%); border: 1px solid hsl(var(--h) 40% 32%); }
+.job-main { min-width: 0; }
+.job-title { margin: 0 0 3px; font-size: 1.05rem; line-height: 1.3; }
+.job-title a { color: var(--text-primary, #e6edf7); text-decoration: none; }
+.job-title a:hover { color: var(--accent-light, #60a5fa); text-decoration: underline; text-underline-offset: 3px; }
+.job-meta { margin: 0 0 8px; display: flex; flex-wrap: wrap; gap: 2px 10px; font-size: .9rem; color: var(--text-secondary, #96a7c2); }
+.job-company { font-weight: 650; color: var(--text-primary, #e6edf7); }
+.job-chips { margin: 0 0 8px; display: flex; flex-wrap: wrap; gap: 6px; }
+.chip { font-size: .76rem; font-weight: 600; padding: 2px 9px; border-radius: 999px; border: 1px solid var(--border-light, #2c3d5c); color: var(--text-secondary, #96a7c2); white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
+.chip-ok { color: var(--success, #22c55e); border-color: color-mix(in srgb, var(--success, #22c55e) 45%, transparent); }
+.chip-pay { color: var(--warning, #f59e0b); border-color: color-mix(in srgb, var(--warning, #f59e0b) 45%, transparent); }
+.chip-tag { color: var(--text-muted, #7386a3); font-weight: 500; }
+.job-excerpt { margin: 0 0 8px; max-width: 80ch; font-size: .92rem; line-height: 1.5; color: var(--text-secondary, #96a7c2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+.job-provenance { margin: 0; font-size: .82rem; }
.job-source { font-weight: 600; }
.job-source { font-weight: 600; }
+@media (max-width: 560px) { .job-item { grid-template-columns: 1fr; } .job-logo { display: none; } }
+@media (prefers-reduced-motion: reduce) { .job-item { transition: none; } }
.job-pager { display: flex; gap: 12px; align-items: center; margin: 18px 0 8px; }
.job-pager { display: flex; gap: 12px; align-items: center; margin: 18px 0 8px; }
.job-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 16px; margin: 16px 0; font-size: .92rem; }
.job-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 16px; margin: 16px 0; font-size: .92rem; }
.job-facts dt { color: var(--text-secondary, #96a7c2); }
.job-facts dt { color: var(--text-secondary, #96a7c2); }
@@ -47,15 +47,26 @@ function provenance(row, source) {
@@ -47,15 +47,26 @@ function provenance(row, source) {
return html`<p class="job-provenance"><span class="muted">Listing from</span> ${sourceLink(row, source)}${row.posted_at ? html` <span class="muted">· posted ${dateWords(row.posted_at)}</span>` : ''}</p>`;
return html`<p class="job-provenance"><span class="muted">Listing from</span> ${sourceLink(row, source)}${row.posted_at ? html` <span class="muted">· posted ${dateWords(row.posted_at)}</span>` : ''}</p>`;
}
}
-/** One listing in a list: title, who and where, the excerpt, the tags, and the link back to the board. */
+/** A company's initial on a tint picked from its name: the same company always gets the same tile. */
+function initialTile(company) {
+ const name = String(company || '?').trim();
+ let h = 0;
+ for (const ch of name) h = (h * 31 + ch.codePointAt(0)) % 360;
+ return html`<span class="job-logo" style="--h:${h}" aria-hidden="true">${(name[0] || '?').toUpperCase()}</span>`;
+}
+
+/** One listing in a list, as a card: who, what and where at a glance, two lines of the excerpt, and the link back. */
function listingItem(row, { source }) {
function listingItem(row, { source }) {
const l = meta(row);
const l = meta(row);
return html`<li class="job-item" id="${l.id}">
return html`<li class="job-item" id="${l.id}">
+${initialTile(l.company)}
+<div class="job-main">
<h3 class="job-title"><a href="/jobs/${l.id}">${l.title}</a></h3>
<h3 class="job-title"><a href="/jobs/${l.id}">${l.title}</a></h3>
-<p class="job-meta"><span class="job-company">${l.company}</span> · <span class="job-location">${l.location || 'Location not given'}</span>${l.remote ? html` · ${badge('Remote', 'ok')}` : ''}${l.job_type ? html` · <span class="job-type">${l.job_type}</span>` : ''}${l.salary ? html` · <span class="job-salary">${l.salary}</span>` : ''}</p>
+<p class="job-meta"><span class="job-company">${l.company}</span><span class="job-location">${l.location || 'Location not given'}</span></p>
+<p class="job-chips">${l.remote ? html`<span class="chip chip-ok">Remote</span>` : ''}${l.job_type ? html`<span class="chip">${l.job_type}</span>` : ''}${l.salary ? html`<span class="chip chip-pay">${l.salary}</span>` : ''}${l.tags.slice(0, 3).map((t) => html`<span class="chip chip-tag">${t}</span>`)}</p>
<p class="job-excerpt">${l.excerpt}</p>
<p class="job-excerpt">${l.excerpt}</p>
-${l.tags.length ? html`<ul class="job-tags">${l.tags.slice(0, 8).map((t) => html`<li>${t}</li>`)}</ul>` : ''}
${provenance(row, source)}
${provenance(row, source)}
+</div>
</li>`;
</li>`;
}
}