diff --git a/public/styles/global.css b/public/styles/global.css index 4e12f63..8977452 100644 --- a/public/styles/global.css +++ b/public/styles/global.css @@ -159,35 +159,130 @@ h1 { line-height: 1.6; } -.hero-stats { - display: grid; - grid-template-columns: repeat(3, minmax(6rem, 1fr)); - gap: 0.75rem; - min-width: 25rem; +.hero-stats-panel { + width: min(52rem, 100%); } -.hero-stats div { +.hero-stats-panel summary { + display: none; +} + +.hero-stats { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.75rem; + width: 100%; +} + +.stat-card { + min-width: 0; padding: 1rem; border: 1px solid var(--border); border-radius: 6px; background: rgba(18, 27, 32, 0.82); } -.hero-stats strong { +.stat-card-total { + display: grid; + place-content: center; + text-align: center; +} + +.stat-card-total strong { display: block; - font-size: 2rem; + font-size: 3rem; line-height: 1; font-variant-numeric: tabular-nums; } -.hero-stats span { +.stat-card-total span, +.stat-heading { color: var(--muted); font-size: 0.8rem; text-transform: uppercase; } +.stat-heading { + display: block; + margin-bottom: 0.75rem; + color: var(--text); + font-weight: 700; +} + +.stat-card-group { + container-type: inline-size; +} + +.stat-list { + display: grid; + gap: 0.45rem; + margin: 0; + padding: 0; + list-style: none; +} + +.stat-list li { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 0.5rem; + color: var(--muted); + font-size: clamp(0.68rem, 4.5cqw, 0.84rem); + line-height: 1.25; +} + +.stat-icon { + position: relative; + display: inline-block; + width: 0.85em; + height: 0.85em; + flex: 0 0 auto; +} + +.stat-icon-platform { + border: 1px solid rgba(33, 198, 183, 0.82); + border-radius: 2px; + box-shadow: 0.18em 0.18em 0 rgba(33, 198, 183, 0.24); +} + +.stat-icon-type { + border: 1px solid rgba(167, 120, 255, 0.86); + background: rgba(167, 120, 255, 0.22); + transform: rotate(45deg) scale(0.82); +} + +.stat-icon-status { + border-radius: 50%; + border: 1px solid rgba(98, 217, 107, 0.9); +} + +.stat-icon-status::after { + content: ""; + position: absolute; + left: 0.22em; + top: 0.16em; + width: 0.28em; + height: 0.46em; + border: solid rgba(98, 217, 107, 0.95); + border-width: 0 0.12em 0.12em 0; + transform: rotate(45deg); +} + +.stat-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.stat-list strong { + color: var(--text); + font-variant-numeric: tabular-nums; +} + .flagships, -.related { +.related, +.browse-summary { margin-bottom: clamp(2rem, 5vw, 4rem); } @@ -212,6 +307,66 @@ h1 { gap: 1rem; } +.browse-summary { + scroll-margin-top: 5.5rem; +} + +.browse-summary-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.browse-facet { + min-width: 0; + padding: 1rem; + border: 1px solid var(--border); + border-radius: 6px; + background: rgba(18, 27, 32, 0.64); +} + +.browse-facet h3 { + font-size: 1.2rem; +} + +.browse-link-list { + display: grid; + gap: 0.45rem; + margin-top: 0.8rem; +} + +.browse-link-list a { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + min-height: 2.15rem; + padding: 0.45rem 0.55rem; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 4px; + background: rgba(4, 8, 10, 0.38); + color: var(--muted); + font-size: 0.88rem; + text-decoration: none; +} + +.browse-link-list a:hover { + border-color: rgba(33, 198, 183, 0.28); + color: var(--text); +} + +.browse-link-list span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.browse-link-list em { + color: #c8d8d3; + font-style: normal; + font-variant-numeric: tabular-nums; +} + .catalog-shell { display: grid; grid-template-columns: 18rem minmax(0, 1fr); @@ -224,6 +379,38 @@ h1 { top: 5.5rem; display: grid; gap: 0.9rem; + max-height: calc(100vh - 6.5rem); + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; +} + +.filter-rail, +.filter-list { + scrollbar-color: rgba(7, 11, 13, 0.82) transparent; + scrollbar-width: thin; +} + +.filter-rail::-webkit-scrollbar, +.filter-list::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.filter-rail::-webkit-scrollbar-track, +.filter-list::-webkit-scrollbar-track { + background: transparent; +} + +.filter-rail::-webkit-scrollbar-thumb, +.filter-list::-webkit-scrollbar-thumb { + border-radius: 999px; + background: rgba(7, 11, 13, 0.72); +} + +.filter-rail::-webkit-scrollbar-thumb:hover, +.filter-list::-webkit-scrollbar-thumb:hover { + background: rgba(38, 51, 55, 0.82); } .filter-panel { @@ -553,18 +740,30 @@ a:focus-visible { grid-template-columns: 1fr; } - .hero-stats { + .hero-stats-panel { min-width: 0; } + .hero-stats { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .filter-rail { position: static; grid-template-columns: repeat(2, minmax(0, 1fr)); + max-height: none; + overflow-y: visible; + overscroll-behavior: auto; + scrollbar-gutter: auto; } .flagship-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + + .browse-summary-grid { + grid-template-columns: 1fr; + } } @media (max-width: 640px) { @@ -581,12 +780,76 @@ a:focus-visible { font-size: 3.1rem; } - .hero-stats, .filter-rail, .flagship-grid { grid-template-columns: 1fr; } + .hero-stats-panel summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + min-height: 2.7rem; + margin-bottom: 0.75rem; + padding: 0.7rem 0.85rem; + border: 1px solid var(--border); + border-radius: 6px; + background: rgba(18, 27, 32, 0.82); + color: var(--muted); + cursor: pointer; + font-size: 0.82rem; + font-weight: 700; + list-style: none; + text-transform: uppercase; + } + + .hero-stats-panel summary::-webkit-details-marker { + display: none; + } + + .hero-stats-panel summary::after { + content: "+"; + color: var(--text); + font-size: 1.2rem; + line-height: 1; + } + + .hero-stats-panel[open] summary::after { + content: "-"; + } + + .hero-stats { + grid-auto-columns: minmax(10.5rem, 72vw); + grid-auto-flow: column; + grid-template-columns: none; + overflow-x: auto; + overflow-y: hidden; + padding-bottom: 0.45rem; + scroll-snap-type: x proximity; + scrollbar-color: rgba(7, 11, 13, 0.82) transparent; + scrollbar-width: thin; + -webkit-overflow-scrolling: touch; + } + + .hero-stats::-webkit-scrollbar { + height: 5px; + } + + .hero-stats::-webkit-scrollbar-track { + background: transparent; + } + + .hero-stats::-webkit-scrollbar-thumb { + border-radius: 999px; + background: rgba(7, 11, 13, 0.72); + } + + .stat-card { + min-height: 10.25rem; + scroll-snap-align: start; + } + .hack-grid { grid-template-columns: 1fr; } diff --git a/src/components/Layout.astro b/src/components/Layout.astro index ccc6a7a..ef8e977 100644 --- a/src/components/Layout.astro +++ b/src/components/Layout.astro @@ -28,8 +28,7 @@ const { diff --git a/src/pages/index.astro b/src/pages/index.astro index 37d9de2..a931455 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -2,12 +2,73 @@ import HackCard from '../components/HackCard.astro'; import FilterGroup from '../components/FilterGroup.astro'; import Layout from '../components/Layout.astro'; -import { catalog, flagshipHacks, getFacetEntries, hacks } from '../lib/site-data'; +import { catalog, facetValueSlug, flagshipHacks, getFacetEntries, hacks } from '../lib/site-data'; +import type { FacetName } from '../lib/types'; const platforms = getFacetEntries('platform'); const bases = getFacetEntries('base').slice(0, 12); const types = getFacetEntries('type'); const statuses = getFacetEntries('status'); + +type StatItem = { + label: string; + count: number; +}; + +type StatGroup = { + label: string; + icon: string; + items: StatItem[]; +}; + +type BrowseGroup = { + label: string; + facet: Extract; + entries: [string, number][]; +}; + +function facetCount(facet: FacetName, value: string): number { + return catalog.facets[facet]?.[value] ?? 0; +} + +function displayFacetValue(value: string): string { + return value === 'Unknown' ? 'Unclassified' : value; +} + +function selectedItems(facet: FacetName, values: string[]): StatItem[] { + return values + .map((value) => ({ label: displayFacetValue(value), count: facetCount(facet, value) })) + .filter((item) => item.count > 0); +} + +function otherItem(facet: FacetName, selectedValues: string[]): StatItem | undefined { + const selected = new Set([...selectedValues, 'Unknown']); + const count = getFacetEntries(facet) + .filter(([value]) => !selected.has(value)) + .reduce((total, [, entryCount]) => total + entryCount, 0); + return count > 0 ? { label: 'Other', count } : undefined; +} + +function summaryItems(facet: FacetName, values: string[]): StatItem[] { + const items = selectedItems(facet, values); + const other = otherItem(facet, values); + const unknown = facetCount(facet, 'Unknown'); + if (other) items.push(other); + if (unknown > 0) items.push({ label: 'Unclassified', count: unknown }); + return items; +} + +const statGroups: StatGroup[] = [ + { label: 'Platforms', icon: 'platform', items: summaryItems('platform', ['GBA', 'NDS', 'PC', 'GBC']) }, + { label: 'Types', icon: 'type', items: summaryItems('type', ['Expansion', 'New Experience', 'Difficulty', 'QoL']) }, + { label: 'Status', icon: 'status', items: summaryItems('status', ['Complete', 'Ongoing', 'Beta']) }, +]; + +const browseGroups: BrowseGroup[] = [ + { label: 'Platforms', facet: 'platform', entries: platforms }, + { label: 'Types', facet: 'type', entries: types }, + { label: 'Status', facet: 'status', entries: statuses }, +]; --- @@ -16,11 +77,26 @@ const statuses = getFacetEntries('status');

ROM Hack Archive

A private catalog of {catalog.count} Pokémon ROM hacks, fan-games, patches, notes, guides, and files.

-
-
{catalog.count}hacks
-
{catalog.facets.platform.GBA ?? 0}GBA
-
{catalog.facets.type.Expansion ?? 0}expansions
-
+
+ Catalog breakdown +
+
{catalog.count}hacks
+ {statGroups.map((group) => ( +
+ {group.label} +
    + {group.items.map((item) => ( +
  • + + {item.label} + {item.count} +
  • + ))} +
+
+ ))} +
+
@@ -33,6 +109,28 @@ const statuses = getFacetEntries('status');
+
+
+

Browse the archive

+

Jump into grouped catalog views without privileging a single platform or play style.

+
+
+ {browseGroups.map((group) => ( +
+

{group.label}

+ +
+ ))} +
+
+