Part 2 of the richer Modrinth tab. Part 1 (PR #61) filtered browse results by
server type; this adds the detail view and, with it, the compatibility verdict.
- shared/mods.ts: pure pickCompatibleVersion(versions, {mcVersion, loaders}).
Same doctrine as diffUpdates - a version_number is arbitrary text and is never
compared as if it sorted. Recency comes from date_published, and a stable
release outranks a newer beta/alpha. Also folderForLoaders(), and PLUGIN_LOADERS
moved here so the update check and the folder decision cannot drift apart.
- core/mods.ts modrinthDetail(): project + versions + members in one call each,
versions fetched UNFILTERED so the UI can tell "nothing for your Minecraft
version" apart from "nothing for your loader". Compatibility is computed
against searchLoaders(type) - the same set browse was filtered by - so a listed
result can never claim a compatibility the install then refuses.
- installModrinth(): now picks via the same pure function, accepts an optional
versionId that is validated against the project's own version list, and takes
the target folder from the chosen version's loaders instead of the server type.
A hybrid (mohist/arclight) runs Bukkit plugins AND Forge mods, so type alone
dropped every plugin into mods/.
- ModsView: expandable detail per result - compatibility line, author, downloads,
followers, license, categories, description, external links. Late replies are
discarded if the card was collapsed meanwhile; switching servers clears results
and detail (they carry the old server's loader verdict).
- en/tr keys in lockstep (42/42).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What
Part 2 of the richer Modrinth tab: opening a browse result now shows a detail view with a compatibility line for this specific server. Closes #47 (part 1 was #61).
shared/mods.ts—pickCompatibleVersion(versions, { mcVersion, loaders })(pure). Filters by loader ∩ MC version, then prefers a stablereleaseover a newerbeta/alpha, and takes recency fromdate_published. Aversion_numberis arbitrary text and is never compared as if it sorted — the same doctrinediffUpdatesalready follows. A version that declares no loaders / no game_versions is not excluded: claiming "incompatible" on missing metadata is worse than showing it.modrinthDetail()(main-side): title, description body, author, downloads, followers, license, categories, links (Modrinth/source/issues/wiki), version count, pluscompatibleandlatestForLoader. Versions are fetched unfiltered and matched locally, so the UI can distinguish "no build for MC 1.21.4" from "no build for your loader at all" — a server-side filtered query collapses both into an empty list. Thememberscall is.catch(() => []): losing the author must not lose the detail.ModsView: expandable panel per result, green "Compatible — X supports MC Y" or an amber line naming the newest build that does exist. Install from the detail passes that exact version id.Why the install path changed too
The compatibility line would have lied without it. PR #61 made a hybrid (mohist/arclight) search
paper ∪ forge, butinstallModrinthstill queried?loaders=["paper"]— so a Forge mod could be listed as compatible and then fail withno-compatible-versionon click. Both paths now consume the same loader set.Same fix removes a second-order bug: the target folder was
MODDED_TYPES.includes(type) ? 'mods' : 'plugins', and mohist is in both families — every Bukkit plugin installed onto a hybrid server landed inmods/. The folder now comes from the chosen version's own loaders (folderForLoaders), which is the only thing that can decide it for a hybrid.installModgained an optionalversionId. It is validated against that project's own version list server-side, so the renderer can choose a version but can never point the download at an arbitrary file — same guardapplyUpdateuses.Verification
npm run typecheck+npm run build— clean.MSMS_SMOKE_MODUPDATE— exit 0. New units: a Paper 1.20.1 server picks the stable paper/spigot build over both a newer Fabric release and a newer Paper beta; an unsupported MC version returnsundefinedwhilelatestForLoaderstill resolves; a loader with no builds returnsundefined; an empty loader filter excludes nothing; a lexically larger version string loses to the newer date. Plus the hybrid folder decision (forge→mods/, bukkit-family→plugins/, missing→fallback).MSMS_SMOKE— exit 0 (renderer still mounts with the rewritten ModsView).modsblock: 42/42, no orphans either way.Disclosed gaps
modrinthDetailis a thin shell over three documented v2 endpoints; the field mapping (icon_url,source_url,license.name,members[].user.username) is read off the API docs, not asserted against a live response in this environment. The pure matching underneath it is what the smoke covers.plugins/.bodyis rendered as plain pre-wrap text; Modrinth serves markdown, so headings/links appear as raw markup. Deliberate — no markdown renderer is being pulled in for this.