diff --git a/docs/greatest.md b/docs/greatest.md index 42496c1..4c4ded4 100644 --- a/docs/greatest.md +++ b/docs/greatest.md @@ -1,6 +1,7 @@ # 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. +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 diff --git a/greatest/catalog.json b/greatest/catalog.json index d3ce4fb..4c81cc6 100644 --- a/greatest/catalog.json +++ b/greatest/catalog.json @@ -351,7 +351,7 @@ "period": "1975–1979", "periodOrder": 2, "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, "cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31469-xYQ8kWQynM7n.png", "chapters": null, @@ -369,7 +369,7 @@ "period": "1975–1979", "periodOrder": 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, "cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx39705-uxZAOISKjKgB.jpg", "chapters": 91, @@ -429,7 +429,7 @@ "period": "1975–1979", "periodOrder": 2, "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, "cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx31388-JDIwg74gvgt3.jpg", "chapters": 44, @@ -447,7 +447,7 @@ "period": "1975–1979", "periodOrder": 2, "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, "cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/bx31365-ucezaL74sIgK.jpg", "chapters": 139, @@ -465,7 +465,7 @@ "period": "1975–1979", "periodOrder": 2, "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, "cover": "https://s4.anilist.co/file/anilistcdn/media/manga/cover/medium/nx30733-QmyPwBhjbgyX.jpg", "chapters": 1968, @@ -2126,7 +2126,7 @@ "period": "2025–2026", "periodOrder": 12, "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": { "kind": "award/info", "url": "https://tsugimanga.jp/winners_announcement/comics" diff --git a/public/greatest.css b/public/greatest.css index 3fc7808..2428a21 100644 --- a/public/greatest.css +++ b/public/greatest.css @@ -11,8 +11,8 @@ .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-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: 22px 16px; } -.greatest-tile { min-width: 0; } -.greatest-open { position: relative; display: block; width: 100%; padding: 0; border: 0; text-align: left; color: inherit; background: none; cursor: pointer; font: inherit; } +.greatest-tile { min-width: 0; display: flex; flex-direction: column; } +.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: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; } @@ -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-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-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.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; } @@ -32,6 +34,7 @@ .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 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-links { display: flex; flex-wrap: wrap; gap: 8px; margin: 17px 0; } @media (max-width: 680px) { diff --git a/public/greatest.js b/public/greatest.js index 240f5cd..d870aa7 100644 --- a/public/greatest.js +++ b/public/greatest.js @@ -77,7 +77,9 @@ 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-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); return h('article', { class: 'greatest-tile' }, open, ratingButtons(item, true)); } @@ -85,6 +87,7 @@ function statusLabel(status) { return status === 'ongoing' ? 'Ongoing' : status === 'completed' ? 'Completed' : 'Status unknown'; } + function plainNote(note) { return String(note || '').replace(/\*/g, ''); } function openDetails(item) { dialog.textContent = ''; @@ -100,7 +103,8 @@ h('div', { class: 'greatest-dialog-body' }, cover(item), h('div', {}, h('h2', {}, item.title), 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}`), 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.')))); diff --git a/test/greatest.test.js b/test/greatest.test.js index a619226..4915696 100644 --- a/test/greatest.test.js +++ b/test/greatest.test.js @@ -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.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 => entry.note?.length >= 45 && /[.!?]$/.test(entry.note)), 'Every catalog title has a complete reason for inclusion'); for (const [index, period] of catalog.periods.entries()) { const entries = catalog.entries.filter(entry => entry.period === period); assert.ok(entries.length >= 5 && entries.length <= 10); diff --git a/tools/test-greatest-ui.js b/tools/test-greatest-ui.js index e17dcdd..2720fd4 100644 --- a/tools/test-greatest-ui.js +++ b/tools/test-greatest-ui.js @@ -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.locator('.greatest-period').count(), 13); 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 page.getByRole('button', { name: '🔥 Flame' }).click(); assert.equal(await page.locator('.greatest-tile').count(), 1); await page.getByRole('button', { name: 'All', exact: true }).click(); await page.getByRole('button', { name: 'Rank by everyone' }).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').getByRole('button', { name: 'Close' }).click(); 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.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'Mobile horizontal overflow'); await page.getByRole('button', { name: '📚 In library' }).click();