From 27f20f1933e96aa6c6d5cc12a1f9f0778495d1f9 Mon Sep 17 00:00:00 2001 From: natebitsats <124692008+natebitsats@users.noreply.github.com> Date: Sat, 16 May 2026 14:19:49 +0100 Subject: [PATCH 01/36] feat(ui): inscription-page UI overhaul + nav improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inscription page (templates/inscription.html, static/index.css, static/index.js): - Gallery row gets two view modes: - Default scroll-strip with horizontal overflow + scroll-snap. ~4 thumbs visible per view on desktop. - Optional grid view toggled inline via a small icon toolbar in the `gallery` dt. Grid paginates at 20 items per page; the same prev/next arrows that scrolled the strip now act as page controls. No URL change — toggle is a CSS class swap with a vanilla-JS state machine. All gallery items are sent in the initial HTML; loading=lazy on iframes scopes actual fetches to the viewport. Replaces the previous `take(4)` + page-reload flow. - Subtitle row: when the inscription has an artwork title (from properties.attributes.title), the existing subtitle paragraph is wrapped in a flex row paired with a right-aligned link-icons div. The icons are populated client-side from the metadata `links` field — each entry parsed as a URL, http(s) only, hostname required (drops bare "@handle" entries). At most one X/Twitter icon (x.com / twitter.com) and one default website icon are surfaced. - Mobile (max-width: 38rem): scroll-strip arrows hidden (touch swipe handles horizontal navigation natively), and an IntersectionObserver triggers a one-shot CSS keyframe ~1.2s after the gallery enters view — the strip nudges 18px left and back via transform. Transform is used instead of an actual scrollBy() because scroll-snap-type: x mandatory snaps any small scroll back to the nearest snap point instantly. Nav (templates/page.html, static/index.js): - New ordinals.com mirror link: a clickable ordinals-circle icon second in the nav, with its href set on DOMContentLoaded to https://ordinals.com — so any page in this fork has a one-click jump to the canonical ordinals.com equivalent. - Brand text updated to "Ordinals•Gallery" and a museum nav link added pointing at museumof.btc. New static assets: - view-strip.svg / view-grid.svg — gallery view-mode toggle icons. - x.svg / link.svg — subtitle-row link icons (X brand + default website). - ordinals.svg — dark-nav-friendly variant of the ordinals circle (#a1adb8 fill hardcoded, no prefers-color-scheme dependency). - museum.svg — Font Awesome museum building-columns glyph. Co-Authored-By: Claude Opus 4.7 (1M context) --- static/index.css | 130 ++++++++++++++++++++++++++++++++++++- static/index.js | 129 ++++++++++++++++++++++++++++++++++++ static/link.svg | 1 + static/museum.svg | 1 + static/ordinals.svg | 1 + static/view-grid.svg | 1 + static/view-strip.svg | 1 + static/x.svg | 1 + templates/inscription.html | 24 +++++-- templates/page.html | 4 +- 10 files changed, 284 insertions(+), 9 deletions(-) create mode 100644 static/link.svg create mode 100644 static/museum.svg create mode 100644 static/ordinals.svg create mode 100644 static/view-grid.svg create mode 100644 static/view-strip.svg create mode 100644 static/x.svg diff --git a/static/index.css b/static/index.css index 1367241350..c7216ab96a 100644 --- a/static/index.css +++ b/static/index.css @@ -302,9 +302,135 @@ a.mythic { } .subtitle { - margin-top: 0.25rem; + margin: 0; } -h1:has(+ .subtitle) { +h1:has(+ .subtitle), h1:has(+ .subtitle-row) { margin-bottom: 0; } + +.subtitle-row { + align-items: center; + display: flex; + gap: 1rem; + margin-top: 0.25rem; +} + +.subtitle-row > .subtitle { + flex: 1; + min-width: 0; +} + +.title-links { + display: flex; + flex-shrink: 0; + gap: 0.75rem; +} + +.title-links > a { + align-items: center; + display: inline-flex; +} + +.title-links img.icon { + height: 1.1rem; + width: 1.1rem; +} + +.gallery-row { + align-items: center; + display: flex; +} + +.gallery-row > .thumbnails { + flex: 1; +} + +.gallery-mode-scroll > .thumbnails { + flex-wrap: nowrap; + overflow-x: auto; + scroll-behavior: smooth; + scroll-snap-type: x mandatory; + scrollbar-width: none; +} + +.gallery-mode-scroll > .thumbnails::-webkit-scrollbar { + display: none; +} + +.gallery-mode-scroll > .thumbnails > a { + flex: 0 0 23%; + scroll-snap-align: center; +} + +.gallery-mode-all > .thumbnails > a[hidden] { + display: none; +} + +.gallery-row .gallery-prev, +.gallery-row .gallery-next { + background: none; + border: none; + color: #a1adb8; + cursor: pointer; + flex-shrink: 0; + font-family: inherit; + font-size: 1.5em; + padding: 0 0.4em; +} + +.gallery-row .gallery-prev[disabled], +.gallery-row .gallery-next[disabled] { + cursor: default; + opacity: 0.3; +} + +dt.with-toolbar { + align-items: center; + display: flex; +} + +.gallery-toolbar { + display: flex; + gap: 0.25em; + margin-left: auto; +} + +.gallery-view-btn { + background: none; + border: 1px solid transparent; + border-radius: 4px; + cursor: pointer; + padding: 0.25em; +} + +.gallery-view-btn:hover { + border-color: #333; +} + +.gallery-view-btn.active { + background: #1f2426; + border-color: #444; +} + +.gallery-view-btn img.icon { + display: block; + height: 18px; + width: 18px; +} + +@media (max-width: 38rem) { + .gallery-mode-scroll > .gallery-prev, + .gallery-mode-scroll > .gallery-next { + display: none; + } +} + +@keyframes gallery-scroll-hint { + 0%, 100% { transform: translateX(0); } + 40% { transform: translateX(-18px); } +} + +.gallery-row.scroll-hint > .thumbnails { + animation: gallery-scroll-hint 900ms ease-in-out; +} diff --git a/static/index.js b/static/index.js index 652bd48a7c..0a784d81d0 100644 --- a/static/index.js +++ b/static/index.js @@ -3,6 +3,135 @@ addEventListener("DOMContentLoaded", () => { time.setAttribute('title', new Date(time.textContent)); } + let ordinalsLink = document.getElementById('ordinals-link'); + if (ordinalsLink) { + ordinalsLink.href = 'https://ordinals.com' + location.pathname + location.search; + } + + let titleLinks = document.querySelector('.title-links'); + if (titleLinks) { + let urls = []; + for (let dt of document.querySelectorAll('dt')) { + if (dt.textContent.trim().toLowerCase() === 'links') { + let dd = dt.nextElementSibling; + if (dd) { + for (let item of dd.querySelectorAll('li, a')) { + let raw = item.tagName === 'A' ? item.href : item.textContent.trim(); + try { + let u = new URL(raw); + if ((u.protocol === 'http:' || u.protocol === 'https:') && u.hostname) { + urls.push(u); + } + } catch (_) {} + } + } + break; + } + } + let x = null, website = null; + for (let u of urls) { + let host = u.hostname.replace(/^www\./, ''); + if (!x && (host === 'x.com' || host === 'twitter.com')) x = u.href; + else if (!website && host !== 'x.com' && host !== 'twitter.com') website = u.href; + } + let entries = []; + if (website) entries.push({href: website, src: '/static/link.svg', label: 'website'}); + if (x) entries.push({href: x, src: '/static/x.svg', label: 'X'}); + for (let e of entries) { + let a = document.createElement('a'); + a.href = e.href; + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + a.title = e.label; + let img = document.createElement('img'); + img.className = 'icon'; + img.src = e.src; + img.alt = e.label; + a.appendChild(img); + titleLinks.appendChild(a); + } + } + + const GALLERY_PAGE_SIZE = 20; + + for (let row of document.querySelectorAll('.gallery-row')) { + let track = row.querySelector('.thumbnails'); + let prevBtn = row.querySelector('.gallery-prev'); + let nextBtn = row.querySelector('.gallery-next'); + if (!track) continue; + + let items = track.querySelectorAll(':scope > a'); + let total = items.length; + let toolbar = row.parentElement.previousElementSibling; + let viewBtns = toolbar && toolbar.classList.contains('with-toolbar') + ? toolbar.querySelectorAll('.gallery-view-btn') + : []; + let mode = 'scroll'; + let page = 0; + let pageCount = Math.max(1, Math.ceil(total / GALLERY_PAGE_SIZE)); + + function applyPage() { + let start = page * GALLERY_PAGE_SIZE; + let end = start + GALLERY_PAGE_SIZE; + items.forEach((item, idx) => { + item.toggleAttribute('hidden', mode === 'all' && (idx < start || idx >= end)); + }); + } + + function updateArrows() { + if (mode === 'scroll') { + let max = track.scrollWidth - track.clientWidth; + if (prevBtn) prevBtn.disabled = track.scrollLeft <= 1; + if (nextBtn) nextBtn.disabled = track.scrollLeft >= max - 1; + } else { + if (prevBtn) prevBtn.disabled = page <= 0; + if (nextBtn) nextBtn.disabled = page >= pageCount - 1; + } + } + + function setMode(nextMode) { + if (mode === nextMode) return; + mode = nextMode; + row.classList.toggle('gallery-mode-scroll', mode === 'scroll'); + row.classList.toggle('gallery-mode-all', mode === 'all'); + viewBtns.forEach(btn => btn.classList.toggle('active', btn.dataset.mode === mode)); + page = 0; + applyPage(); + updateArrows(); + } + + if (prevBtn) prevBtn.addEventListener('click', () => { + if (mode === 'scroll') { + track.scrollBy({left: -track.clientWidth, behavior: 'smooth'}); + } else if (page > 0) { + page--; applyPage(); updateArrows(); + } + }); + if (nextBtn) nextBtn.addEventListener('click', () => { + if (mode === 'scroll') { + track.scrollBy({left: track.clientWidth, behavior: 'smooth'}); + } else if (page < pageCount - 1) { + page++; applyPage(); updateArrows(); + } + }); + viewBtns.forEach(btn => btn.addEventListener('click', () => setMode(btn.dataset.mode))); + + track.addEventListener('scroll', updateArrows); + addEventListener('resize', updateArrows); + updateArrows(); + + if (matchMedia('(max-width: 38rem)').matches && track.scrollWidth > track.clientWidth) { + let observer = new IntersectionObserver((entries) => { + for (let entry of entries) { + if (!entry.isIntersecting) continue; + observer.disconnect(); + setTimeout(() => row.classList.add('scroll-hint'), 1200); + } + }, {threshold: 0.5}); + observer.observe(track); + } + } + let next = document.querySelector('a.next'); let prev = document.querySelector('a.prev'); diff --git a/static/link.svg b/static/link.svg new file mode 100644 index 0000000000..f62f24f2b3 --- /dev/null +++ b/static/link.svg @@ -0,0 +1 @@ + diff --git a/static/museum.svg b/static/museum.svg new file mode 100644 index 0000000000..dea99a0a29 --- /dev/null +++ b/static/museum.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/ordinals.svg b/static/ordinals.svg new file mode 100644 index 0000000000..208f1aedbb --- /dev/null +++ b/static/ordinals.svg @@ -0,0 +1 @@ + diff --git a/static/view-grid.svg b/static/view-grid.svg new file mode 100644 index 0000000000..adde03b099 --- /dev/null +++ b/static/view-grid.svg @@ -0,0 +1 @@ + diff --git a/static/view-strip.svg b/static/view-strip.svg new file mode 100644 index 0000000000..7b787b9fdf --- /dev/null +++ b/static/view-strip.svg @@ -0,0 +1 @@ + diff --git a/static/x.svg b/static/x.svg new file mode 100644 index 0000000000..c706402f77 --- /dev/null +++ b/static/x.svg @@ -0,0 +1 @@ + diff --git a/templates/inscription.html b/templates/inscription.html index 439c018b71..9f8d6f8eac 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -1,6 +1,9 @@

Inscription {{ self.number }}

%% if let Some(title) = &self.properties.attributes.title { -

{{ title }}

+
+

{{ title }}

+ +
%% }
%% if let Some(previous) = self.previous { @@ -43,12 +46,21 @@

Inscription {{ self.number }}

%% } %% if !self.properties.gallery.is_empty() { -
gallery
+
gallery + +
-
-%% for (i, item) in self.properties.gallery.iter().enumerate().take(4) { - {{ Iframe::item(self.id, i, item.id()) }} -%% } +
all ({{ self.properties.gallery.len() }}) diff --git a/templates/page.html b/templates/page.html index 4087b75d3e..1bcb1e1a54 100644 --- a/templates/page.html +++ b/templates/page.html @@ -20,11 +20,13 @@