Show Greatest inclusion reasons on catalog tiles

This commit is contained in:
Codex committed 2026-10-09 10:25:18 +02:00
1 parent f713d0c0a9
commit bedd0bc3b3
6 files changed
+26 -11

No files matched your search

+1
View File
@@ -1,6 +1,7 @@
# Shijō Saikō no Manga — Greatest of All Time # Shijō Saikō no Manga — Greatest of All Time
`greatest.html` is a second, single-page dashboard. The Japanese title is **史上最高の漫画**, romanized **Shijō Saikō no Manga** (“greatest manga of all time”). It displays the [research reading map](manga-reading-map-2026-10-09.md) as ranked five-year cohorts from before 1970 through 2025–2026. Each period keeps its own separator and tile grid. `greatest.html` is a second, single-page dashboard. The Japanese title is **史上最高の漫画**, romanized **Shijō Saikō no Manga** (“greatest manga of all time”). It displays the [research reading map](manga-reading-map-2026-10-09.md) as ranked five-year cohorts from before 1970 through 2025–2026. Each period keeps its own separator and tile grid.
Each tile shows a four-line **Why it matters** preview from the editorial rationale; opening the tile shows the complete text under the same heading. The note is a reason for inclusion, not a plot synopsis or a machine-generated rating.
## Separation and data ownership ## Separation and data ownership
+6 -6
View File
@@ -351,7 +351,7 @@
"period": "1975–1979", "period": "1975–1979",
"periodOrder": 2, "periodOrder": 2,
"rank": 1, "rank": 1,
"note": "Legendary acting rivalry and unusually intense long-form character growth;", "note": "A landmark acting rivalry whose unusually intense long-form character growth keeps generations of readers invested.",
"source": null, "source": null,
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31469-xYQ8kWQynM7n.png", "cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31469-xYQ8kWQynM7n.png",
"chapters": null, "chapters": null,
@@ -369,7 +369,7 @@
"period": "1975–1979", "period": "1975–1979",
"periodOrder": 2, "periodOrder": 2,
"rank": 2, "rank": 2,
"note": "Melancholy train-through-space fables about bodies, technology, and mortality;", "note": "Leiji Matsumoto's melancholy train-through-space fables made bodies, technology, and mortality the heart of a sweeping science-fiction journey.",
"source": null, "source": null,
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx39705-uxZAOISKjKgB.jpg", "cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx39705-uxZAOISKjKgB.jpg",
"chapters": 91, "chapters": 91,
@@ -429,7 +429,7 @@
"period": "1975–1979", "period": "1975–1979",
"periodOrder": 2, "periodOrder": 2,
"rank": 5, "rank": 5,
"note": "A shōjo melodrama landmark with cross-generational popularity;", "note": "A shōjo melodrama landmark whose orphan heroine and far-reaching emotional stakes resonated across generations.",
"source": null, "source": null,
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx31388-JDIwg74gvgt3.jpg", "cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx31388-JDIwg74gvgt3.jpg",
"chapters": 44, "chapters": 44,
@@ -447,7 +447,7 @@
"period": "1975–1979", "period": "1975–1979",
"periodOrder": 2, "periodOrder": 2,
"rank": 6, "rank": 6,
"note": "A historically important, emotionally severe early boys'-love work;", "note": "An emotionally severe early boys'-love landmark that widened the psychological ambition of shōjo manga.",
"source": null, "source": null,
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31365-ucezaL74sIgK.jpg", "cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31365-ucezaL74sIgK.jpg",
"chapters": 139, "chapters": 139,
@@ -465,7 +465,7 @@
"period": "1975–1979", "period": "1975–1979",
"periodOrder": 2, "periodOrder": 2,
"rank": 7, "rank": 7,
"note": "Everyday police-station comedy whose extraordinary longevity makes it a time capsule; full", "note": "An everyday police-station comedy whose extraordinary longevity makes successive decades of Japanese life a manga time capsule.",
"source": null, "source": null,
"cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx30733-QmyPwBhjbgyX.jpg", "cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx30733-QmyPwBhjbgyX.jpg",
"chapters": 1968, "chapters": 1968,
@@ -2126,7 +2126,7 @@
"period": "2025–2026", "period": "2025–2026",
"periodOrder": 12, "periodOrder": 12,
"rank": 3, "rank": 3,
"note": "A bus-ride, almost-romance comedy; #2 in the same reader vote.;", "note": "An understated almost-romance built from everyday bus rides; its 2026 reader-vote finish makes it a promising newcomer rather than an established classic.",
"source": { "source": {
"kind": "award/info", "kind": "award/info",
"url": "https://tsugimanga.jp/winners_announcement/comics" "url": "https://tsugimanga.jp/winners_announcement/comics"
+5 -2
View File
@@ -11,8 +11,8 @@
.greatest-period-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 16px; } .greatest-period-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
.greatest-period-heading h2 { margin: 0; font-size: 21px; } .greatest-period-heading h2 { margin: 0; font-size: 21px; }
.greatest-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 22px 16px; } .greatest-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 22px 16px; }
.greatest-tile { min-width: 0; } .greatest-tile { min-width: 0; display: flex; flex-direction: column; }
.greatest-open { position: relative; display: block; width: 100%; padding: 0; border: 0; text-align: left; color: inherit; background: none; cursor: pointer; font: inherit; } .greatest-open { position: relative; display: block; flex: 1; width: 100%; padding: 0; border: 0; text-align: left; color: inherit; background: none; cursor: pointer; font: inherit; }
.greatest-open:hover .greatest-cover img { transform: scale(1.035); } .greatest-open:hover .greatest-cover img { transform: scale(1.035); }
.greatest-open:focus-visible, .greatest-vote:focus-visible { outline: 2px solid #fff; outline-offset: 3px; } .greatest-open:focus-visible, .greatest-vote:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
.greatest-cover { position: relative; display: block; width: 100%; aspect-ratio: 2 / 3; overflow: hidden; border-radius: 9px; background: linear-gradient(150deg, #49343c, #282c38 60%, #18181d); box-shadow: 0 4px 17px #0007; } .greatest-cover { position: relative; display: block; width: 100%; aspect-ratio: 2 / 3; overflow: hidden; border-radius: 9px; background: linear-gradient(150deg, #49343c, #282c38 60%, #18181d); box-shadow: 0 4px 17px #0007; }
@@ -21,6 +21,8 @@
.greatest-owned { position: absolute; top: 7px; right: 7px; padding: 2px 7px; border-radius: 9px; background: #183d34; color: #a7f0d1; font-size: 11px; font-weight: 700; box-shadow: 0 1px 8px #0008; } .greatest-owned { position: absolute; top: 7px; right: 7px; padding: 2px 7px; border-radius: 9px; background: #183d34; color: #a7f0d1; font-size: 11px; font-weight: 700; box-shadow: 0 1px 8px #0008; }
.greatest-name { display: block; margin-top: 8px; font-weight: 700; line-height: 1.25; } .greatest-name { display: block; margin-top: 8px; font-weight: 700; line-height: 1.25; }
.greatest-meta { display: block; margin-top: 2px; color: var(--muted); font-size: 12px; } .greatest-meta { display: block; margin-top: 2px; color: var(--muted); font-size: 12px; }
.greatest-reason-label { display: block; margin-top: 10px; color: #e9a47d; font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
.greatest-reason { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 4; line-clamp: 4; margin-top: 3px; color: var(--fg); font-size: 12px; line-height: 1.4; }
.greatest-votes { display: flex; gap: 8px; margin-top: 12px; } .greatest-votes { display: flex; gap: 8px; margin-top: 12px; }
.greatest-votes.compact { gap: 3px; margin-top: 6px; } .greatest-votes.compact { gap: 3px; margin-top: 6px; }
.greatest-vote { display: inline-grid; place-items: center; flex: 1; min-width: 0; min-height: 36px; border: 1px solid var(--line); border-radius: 8px; background: var(--card); color: var(--fg); cursor: pointer; font: inherit; } .greatest-vote { display: inline-grid; place-items: center; flex: 1; min-width: 0; min-height: 36px; border: 1px solid var(--line); border-radius: 8px; background: var(--card); color: var(--fg); cursor: pointer; font: inherit; }
@@ -32,6 +34,7 @@
.greatest-dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px; } .greatest-dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px; }
.greatest-dialog-body { display: grid; grid-template-columns: minmax(130px, 230px) minmax(0, 1fr); gap: 22px; } .greatest-dialog-body { display: grid; grid-template-columns: minmax(130px, 230px) minmax(0, 1fr); gap: 22px; }
.greatest-dialog-body h2 { margin: 0 0 8px; font-size: 25px; } .greatest-dialog-body h2 { margin: 0 0 8px; font-size: 25px; }
.greatest-reason-heading { margin: 18px 0 3px; font-size: 15px; color: #e9a47d; }
.greatest-dialog-body p { margin: 9px 0; } .greatest-dialog-body p { margin: 9px 0; }
.greatest-links { display: flex; flex-wrap: wrap; gap: 8px; margin: 17px 0; } .greatest-links { display: flex; flex-wrap: wrap; gap: 8px; margin: 17px 0; }
@media (max-width: 680px) { @media (max-width: 680px) {
+6 -2
View File
@@ -77,7 +77,9 @@
item.library ? h('span', { class: 'greatest-owned', title: 'Already in your library' }, '📚 In library') : null, item.library ? h('span', { class: 'greatest-owned', title: 'Already in your library' }, '📚 In library') : null,
h('span', { class: 'greatest-name' }, item.title), h('span', { class: 'greatest-name' }, item.title),
h('span', { class: 'greatest-meta' }, `${item.year} · ${item.chapters == null ? 'Chapters unknown' : `${item.chapters} chapters`}`), h('span', { class: 'greatest-meta' }, `${item.year} · ${item.chapters == null ? 'Chapters unknown' : `${item.chapters} chapters`}`),
h('span', { class: 'greatest-meta' }, statusLabel(item.status))); h('span', { class: 'greatest-meta' }, statusLabel(item.status)),
h('span', { class: 'greatest-reason-label' }, 'Why it matters'),
h('span', { class: 'greatest-reason' }, plainNote(item.note)));
open.onclick = () => openDetails(item); open.onclick = () => openDetails(item);
return h('article', { class: 'greatest-tile' }, open, ratingButtons(item, true)); return h('article', { class: 'greatest-tile' }, open, ratingButtons(item, true));
} }
@@ -85,6 +87,7 @@
function statusLabel(status) { function statusLabel(status) {
return status === 'ongoing' ? 'Ongoing' : status === 'completed' ? 'Completed' : 'Status unknown'; return status === 'ongoing' ? 'Ongoing' : status === 'completed' ? 'Completed' : 'Status unknown';
} }
function plainNote(note) { return String(note || '').replace(/\*/g, ''); }
function openDetails(item) { function openDetails(item) {
dialog.textContent = ''; dialog.textContent = '';
@@ -100,7 +103,8 @@
h('div', { class: 'greatest-dialog-body' }, cover(item), h('div', {}, h('div', { class: 'greatest-dialog-body' }, cover(item), h('div', {},
h('h2', {}, item.title), h('h2', {}, item.title),
h('p', { class: 'muted' }, `${item.year} · ${item.chapters == null ? 'Chapter count unknown' : `${item.chapters} chapters`} · ${statusLabel(item.status)}`), h('p', { class: 'muted' }, `${item.year} · ${item.chapters == null ? 'Chapter count unknown' : `${item.chapters} chapters`} · ${statusLabel(item.status)}`),
h('p', {}, String(item.note || '').replace(/\*/g, '')), h('h3', { class: 'greatest-reason-heading' }, 'Why it matters'),
h('p', {}, plainNote(item.note)),
h('p', { class: 'muted small' }, `Community votes: 🔥 ${votes.flame || 0} · 👍 ${votes.good || 0} · ● ${votes.neutral || 0} · 💀 ${votes.skip || 0}`), h('p', { class: 'muted small' }, `Community votes: 🔥 ${votes.flame || 0} · 👍 ${votes.good || 0} · ● ${votes.neutral || 0} · 💀 ${votes.skip || 0}`),
ratingButtons(item), links, ratingButtons(item), links,
h('p', { class: 'muted small' }, 'Chapter counts and status come from public catalog metadata, or from your local library when present. Availability may vary by region.')))); h('p', { class: 'muted small' }, 'Chapter counts and status come from public catalog metadata, or from your local library when present. Availability may vary by region.'))));
+1
View File
@@ -8,6 +8,7 @@ test('curated manga catalog has unique identities and ordered five-year periods'
assert.equal(catalog.periods.length, 13); assert.equal(catalog.periods.length, 13);
assert.equal(catalog.entries.length, 106); assert.equal(catalog.entries.length, 106);
assert.ok(catalog.entries.every(entry => /^https:\/\//.test(entry.cover)), 'Every catalog title has cover artwork'); assert.ok(catalog.entries.every(entry => /^https:\/\//.test(entry.cover)), 'Every catalog title has cover artwork');
assert.ok(catalog.entries.every(entry => entry.note?.length >= 45 && /[.!?]$/.test(entry.note)), 'Every catalog title has a complete reason for inclusion');
for (const [index, period] of catalog.periods.entries()) { for (const [index, period] of catalog.periods.entries()) {
const entries = catalog.entries.filter(entry => entry.period === period); const entries = catalog.entries.filter(entry => entry.period === period);
assert.ok(entries.length >= 5 && entries.length <= 10); assert.ok(entries.length >= 5 && entries.length <= 10);
+7 -1
View File
@@ -45,16 +45,22 @@ async function freePort() {
assert.equal(await page.getByRole('link', { name: 'Shijō Saikō no Manga' }).textContent(), 'Shijō Saikō no Manga'); assert.equal(await page.getByRole('link', { name: 'Shijō Saikō no Manga' }).textContent(), 'Shijō Saikō no Manga');
assert.equal(await page.locator('.greatest-period').count(), 13); assert.equal(await page.locator('.greatest-period').count(), 13);
assert.equal(await page.locator('.greatest-tile').count(), 106); assert.equal(await page.locator('.greatest-tile').count(), 106);
const astro = page.locator('.greatest-tile').filter({ hasText: 'Astro Boy' }); const astro = page.locator('.greatest-tile').filter({ has: page.getByRole('button', { name: 'Details for Astro Boy' }) });
assert.equal(await astro.locator('.greatest-reason').isVisible(), true);
assert.match(await astro.locator('.greatest-reason').textContent(), /long-form adventure manga/);
assert.equal(await astro.locator('.greatest-reason-label').textContent(), 'Why it matters');
await astro.getByRole('button', { name: 'Definitely want: Astro Boy' }).click(); await astro.getByRole('button', { name: 'Definitely want: Astro Boy' }).click();
await page.getByRole('button', { name: '🔥 Flame' }).click(); await page.getByRole('button', { name: '🔥 Flame' }).click();
assert.equal(await page.locator('.greatest-tile').count(), 1); assert.equal(await page.locator('.greatest-tile').count(), 1);
await page.getByRole('button', { name: 'All', exact: true }).click(); await page.getByRole('button', { name: 'All', exact: true }).click();
await page.getByRole('button', { name: 'Rank by everyone' }).click(); await page.getByRole('button', { name: 'Rank by everyone' }).click();
await page.getByRole('button', { name: 'Details for Astro Boy' }).click(); await page.getByRole('button', { name: 'Details for Astro Boy' }).click();
await page.getByRole('dialog').getByRole('heading', { name: 'Why it matters' }).waitFor();
assert.match(await page.getByRole('dialog').textContent(), /foundation for the Pluto branch/);
await page.getByRole('dialog').getByText('Community votes: 🔥 1').waitFor(); await page.getByRole('dialog').getByText('Community votes: 🔥 1').waitFor();
await page.getByRole('dialog').getByRole('button', { name: 'Close' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Close' }).click();
await page.setViewportSize({ width: 390, height: 844 }); await page.setViewportSize({ width: 390, height: 844 });
assert.equal(await astro.locator('.greatest-reason').isVisible(), true);
assert.equal(await page.locator('.nav-greatest').evaluate(el => getComputedStyle(el, '::after').content), '"Saikō"'); assert.equal(await page.locator('.nav-greatest').evaluate(el => getComputedStyle(el, '::after').content), '"Saikō"');
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Mobile horizontal overflow'); assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Mobile horizontal overflow');
await page.getByRole('button', { name: '📚 In library' }).click(); await page.getByRole('button', { name: '📚 In library' }).click();