Skip to content

Inscription-page UI: gallery view toggle, subtitle link icons, ordinals.com mirror - #6

Open
natebitsats wants to merge 36 commits into
TatianaMoroz:tatiana-masterfrom
OrdinalsGallery:feature/inscription-page-ui
Open

Inscription-page UI: gallery view toggle, subtitle link icons, ordinals.com mirror#6
natebitsats wants to merge 36 commits into
TatianaMoroz:tatiana-masterfrom
OrdinalsGallery:feature/inscription-page-ui

Conversation

@natebitsats

@natebitsats natebitsats commented May 16, 2026

Copy link
Copy Markdown

Summary

Front-end enhancements for the inscription page and nav. All server-rendered + small vanilla JS, no framework / build step. Rebased onto current tatiana-master. Some likely overlap with your recent UI work — happy to drop anything that duplicates what you already have.

Gallery section on the inscription page

  • Two view modes — scroll-strip (default) and grid — toggled by a small icon pair in the gallery <dt>. No URL change, no reload; same items in the DOM, CSS class flip.
  • Grid mode paginates at 20 items per page using the same prev/next arrows that scrolled the strip.
  • Mobile (max-width: 38rem): arrows hidden in scroll mode (swipe handles it), and the strip plays a one-shot 18px nudge ~1.2s after it scrolls into view as a scrollability hint. Implemented via IntersectionObserver + CSS transform keyframe (transform, not scrollBy, because scroll-snap-type: x mandatory would snap the nudge back instantly).
  • Replaces the previous take(4) + page-reload-via-/gallery flow on the inscription page (the standalone /gallery/<id> route is untouched).

Subtitle-row link icons

  • When an inscription has properties.attributes.title, your existing subtitle paragraph is wrapped in a flex row paired with a right-aligned link-icons div.
  • Icons are populated client-side from the metadata links field. Filter: must parse as URL with http(s) scheme and a non-empty hostname (drops bare @handle entries some galleries have). Per-host routing — at most one X/Twitter (x.com / twitter.com) and one default website. Easy to extend with more brands by adding host checks + an SVG.

Ordinals.com nav mirror

  • Clickable ordinals-circle icon second in the nav. JS sets its href on DOMContentLoaded to https://ordinals.com<current path + search>, so every page has a one-click jump to the canonical ordinals.com equivalent.

Brand + nav

  • Brand text → Ordinals•Gallery.
  • Museum nav icon → museumof.btc.

New static assets

view-strip.svg, view-grid.svg, x.svg, link.svg, ordinals.svg, museum.svg.

Purely templates / static / CSS / JS — no Rust changes. Your oembed_link() / inscription-embed.js / nav a.active highlighting / new .subtitle rule / new embed <dt> are all preserved.

Test plan

  • Hard-refresh an inscription page that has a multi-item gallery. Confirm strip scrolls on desktop, arrows step by one viewport width.
  • Click the grid-view icon — view flips in place, no URL change. Arrows paginate at 20/page.
  • Resize to mobile width — strip arrows disappear, scroll-hint plays ~1.2s after gallery enters view (only once per page-load).
  • On an inscription with links: [https://example.com, https://x.com/handle] in metadata, confirm subtitle row shows both icons; clicking opens in new tab.
  • On an inscription whose metadata links contain a bare @handle string, confirm it's dropped (not rendered with the default website icon).
  • Click the ordinals-circle nav icon — opens ordinals.com/<same-path>.

🤖 Generated with Claude Code

natebitsats and others added 30 commits May 16, 2026 14:19
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<current pathname + search> — 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) <noreply@anthropic.com>
… inscription arrows

Theme switcher (templates/page.html, static/theme.svg, static/theme-init.js,
static/index.css, static/index.js):
- Three modes via data-theme on <html>: ord (default, no attribute), light,
  dark. Cycles via a nav button.
- light: pure white bg/nav, black text/icons (filter: brightness(0)), bitcoin
  orange links (#f2a900 overrides --link), thin #ddd nav divider, 1px black
  border with 2% radius on the main inscription iframe.
- dark: keeps ord's #131516 body bg (so artwork still pops on the familiar
  dark canvas), but nav goes pure black with a thin ordinals#333 divider; text and
  icons become white via the same filter trick (invert(1)).
- Persistence: localStorage. To avoid flash-of-default, theme-init.js is
  loaded synchronously in <head> *before* the stylesheet (external file
  because ord serves default-src 'self' CSP which blocks inline scripts).

Gallery item page (src/templates/item.rs, src/subcommand/server.rs,
templates/item.html):
- ItemHtml gains gallery_title, items, total fields populated from the
  parent inscription's properties; PageContent::title returns the
  breadcrumb form.
- H1 stays "Gallery N Item I" (matches ord conventions). Breadcrumb
  "<gallery_title> / <item_title>" moves into the subtitle paragraph, with
  the gallery part rendered as an <a> back to /inscription/<gallery_id>.
- Side-arrows on the item page cycle through gallery items
  (/gallery/<id>/<i±1>) instead of by inscription number. First/last
  item's outer slot renders as an inert <div>.
- Full gallery slider (scroll-strip + grid view-mode toggle) is rendered
  again inside the item page's <dl> as a <dt>items</dt> entry — you can
  jump to any item from any item, not just from the parent.
- Subtitle-row carries a data-ord-path override on its empty .title-links
  div pointing at /inscription/<item-id>, so the auto-populated ordinals.com
  link goes to the item's *inscription* on ordinals.com (the
  /gallery/<id>/<i> URL doesn't exist there).

Copy buttons on long-id fields (static/index.js, static/index.css,
static/copy.svg, templates/item.html):
- JS injects a small copy icon next to every .collapse element. Click
  writes the un-truncated original text via navigator.clipboard.writeText.
- Visual feedback: .copied class for 1.5s (opacity 1 + title "copied!").
- resize() patched to write to the first text-node child instead of via
  node.textContent — otherwise the truncation pass would wipe the
  appended button on every viewport resize.
- item.html's inscription/gallery <dd> entries get class=collapse so they
  inherit the same truncate+copy treatment.

Inscription side-arrows (static/index.css, templates/item.html):
- Inscription page: opacity:0 by default, opacity:1 on hover (matches
  canonical ord behavior).
- Gallery item page: .inscription div carries a gallery-item-nav modifier
  class that overrides to opacity:0.3 / opacity:1 — cycling is the primary
  action there, so the arrows stay always-visible.
- Both share #ffffff/#000000 hover colour (theme-aware) with 200ms
  transition, plus a 0.5rem gap from the iframe.

Children section gets the gallery-row treatment too (templates/inscription.html,
src/templates/inscription.rs test): scroll-strip / grid view-mode toggle on
the children list, paired with a bump from .take(4) to .take(100) in
src/index.rs and a data-load-more-url hook for the JS state machine to fetch
further batches from /r/children/<id> as the user scrolls.

iframe transparency (static/preview-image.js):
- When loaded as an iframe (window !== window.top), the preview document
  switches its html bg to transparent so the parent page's theme bg shows
  through transparent PNGs (punks etc.). When viewed standalone, the dark
  bg behavior is preserved.

New static assets: copy.svg, theme.svg, theme-init.js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a sort control to the All Inscriptions listing — canonical ordinals.com
has no such option. Default is newest (matches previous behaviour); selecting
"oldest" reverses iteration over the sequence-number table and surfaces a
?sort=oldest query parameter that's preserved through pagination links.

Rust types (src/templates/inscriptions.rs):
- New Sort enum {Newest (default), Oldest} with serde lowercase rename and a
  Display impl for URL rendering.
- InscriptionsHtml gains a `sort: Sort` field plus two helpers:
  - sort_query() — "?sort=oldest" for Oldest, "" for Newest (so the default
    URL stays clean).
  - selected_if(s) — " selected" attribute fragment for the dropdown option
    that matches the current sort.

Server (src/subcommand/server.rs):
- InscriptionsQuery { sort: Option<InscriptionsSort> } extractor on both
  /inscriptions and /inscriptions/<page> routes. Defaults via
  unwrap_or_default().
- Sort flows through to get_inscriptions_paginated and into the rendered
  InscriptionsHtml.

Index (src/index.rs):
- get_inscriptions_paginated takes a Sort parameter. Newest path is
  unchanged (reverse walk from highest sequence number). Oldest path walks
  forward from page_size * page_index for page_size + 1 items (the +1
  detects "more"). No schema change.
- Test callers updated to pass Sort::Newest.

Template (templates/inscriptions.html):
- <form class=sort-form action=/inscriptions method=get> with a labelled
  <select name=sort>. Selected option pre-populates via selected_if().
- Prev/next anchors carry sort_query() so paging within an oldest view
  keeps the sort.

JS (static/index.js):
- Generic auto-submit on any .sort-form select change. Replaces inline
  onchange (CSP would block it). Reusable for any future sort controls.

CSS (static/index.css):
- .sort-form right-aligned via flex. Native <select> themed with --light-bg
  / --light-fg so it reads correctly in ord / light / dark themes.

Re-exports Sort as InscriptionsSort from src/templates.rs to keep server
imports tidy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, renamed sorts

Adds a filter bar to /inscriptions matching ord.net's pattern. All filters
compose with each other and with sort.

Filter struct (src/templates/inscriptions.rs):
- cursed: bool — matches entries with inscription_number < 0.
- from / to: Option<i32> — inclusive range on inscription_number.
- rarity: Rarity (Any default, plus Uncommon/Rare/Epic/Legendary/Mythic) —
  checks the matching Charm bit on the entry's charms bitmask. Cheap because
  the rarity charm bit is set during indexing whenever sat-index is on.
- Filter::matches(&InscriptionEntry) is cheap on every field: no extra db
  lookup needed, all match data is already in the entry.

Index method (src/index.rs::get_inscriptions_paginated):
- New filter parameter. When filter.is_unfiltered() takes the original
  range-based fast path. Otherwise walks the sequence-number table newest-
  or oldest-first, applies filter.matches, skips page_index * page_size
  matches, collects page_size + 1 for "more" detection.

Server (src/subcommand/server.rs):
- InscriptionsQuery expanded: { sort, cursed (with deserialize_truthy that
  accepts 1/true/on/yes/only), from, to, rarity }.
- Server passes server_config.index_sats to the template so the satributes
  select renders only when sat-index is on (where the rarity charms are
  actually populated).

Template (templates/inscriptions.html):
- .inscriptions-toolbar form with: cursed checkbox-as-button (skull icon),
  range <details> popover with from/to inputs + Apply button, conditional
  satributes <select>, sort <select>.
- Sort labels updated to ord.net wording — "Recently Inscribed" /
  "Earliest Inscribed". URL values remain newest/oldest for back-compat.
- pagination_query() helper composes every active filter into prev/next
  anchor URLs; defaults are omitted so canonical URL stays clean.

Auto-submit (static/index.js): generalised to listen on select +
input[type=checkbox] inside both .sort-form and .inscriptions-toolbar.
Number inputs are explicitly excluded — typing partial values shouldn't
fire submits.

CSS (static/index.css): toolbar themed via existing --light-bg / --light-fg
so it adapts to ord / light / dark automatically. .cursed-toggle.active
inverts fg/bg for unmistakable active state.

Deferred to a future session with rationale documented:
- filetype filter (image/motion/text/audio/object/code): needs a new
  CONTENT_TYPE_TO_SEQUENCE_NUMBERS index table — schema change requires
  reindex.
- file-size sort (largest/smallest): same — content_length isn't in the
  entry.

New static asset: skull.svg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… wiring

Per design review: lock in the filter-toolbar *design* and let Michael wire
the backend, keeping divergence from upstream ord minimal so future ord
updates merge cleanly.

Reverted (backend wiring from the previous commit):
- src/index.rs — get_inscriptions_paginated back to (page_size, page_index,
  sort); dropped the Filter parameter and walk-and-filter path.
- src/subcommand/server.rs — InscriptionsQuery back to { sort } only;
  dropped cursed/from/to/rarity fields and deserialize_truthy.
- src/templates/inscriptions.rs — dropped Filter struct, Rarity enum, and
  the filter/index_sats fields + helpers on InscriptionsHtml. Sort wiring
  (already approved) stays.

Kept as design (templates/inscriptions.html + CSS + JS):
- Filter toolbar markup: cursed checkbox-as-button (skull), range <details>
  popover with from/to inputs, satributes <select>, sort <select>. Correct
  name attributes so backend wiring is a drop-in later.
- Only the sort <select> auto-submits (it's wired). Cursed/rarity are inert
  visual placeholders for now; cursed active-state is pure CSS via
  :has(input:checked) so it still demos.

Design fixes from review:
- Toolbar moved to its own row *below* the H1 (was inline with the heading).
- Restored the H1's natural top margin (the header flex wrapper had zeroed it).
- Unified every control to a 2rem height with shared border/radius so the
  skull toggle, Range button, and the two selects line up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Local patch — in practice no one observes the testnet4 1 KB
inscription content limit any more, so it no longer functions
as spam protection. Lifting it lets us dry-run real-size MoBA
galleries on testnet4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bitcoin Core v30+ rejects labels on inactive descriptors with
`Internal addresses should not have a label` (-8), which broke
batch inscriptions against v30 regtest/mainnet nodes. The label
was cosmetic only, so we drop it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make inscription/gallery/item pages lead with the human-readable title
and reconstruct hierarchy from the parent-child graph instead of baking
paths into titles.

Inscription page:
- When a title (Tag 17 properties) is set, show it as the <h1> and demote
  "Inscription N" to the subtitle; untitled inscriptions keep "Inscription N"
  as the heading.

Gallery page:
- Heading becomes "<title> / Gallery" with the title linking to the gallery
  inscription; falls back to "Inscription N / Gallery" when untitled.

Item page:
- Item title becomes the <h1>; subtitle shows "<gallery> / Item N" with the
  gallery linked.
- Remove the per-item "items" carousel, which rendered an iframe for every
  sibling item (e.g. 224 for Bitcoin Shrooms) and dominated page-load time;
  the prev/next arrows already cover item navigation. Drops the now-unused
  ItemHtml.items field.

Breadcrumb trail:
- Walk the parent graph server-side and render a subtle breadcrumb below the
  inscription image (root -> leaf), each ancestor linked, current shown plain.
- Multi-parent inscriptions render one trail per path (depth/trail capped at
  10/5). Untitled ancestors fall back to "#<number>".
- Links inherit the foreground colour, underlined on hover only.

Tests:
- Add with_title / with_breadcrumbs (inscription), with_title (gallery),
  body_without_item_title (item), and the
  inscription_breadcrumb_falls_back_to_number_when_untitled integration test.
- Fix 6 pre-existing inscription template test failures whose expectations
  predated the earlier UI overhaul (missing subtitle-row/title-links block,
  unquoted data-load-more-url).
…bile polish

Titles from text content:
- Short text/plain inscriptions (single line, <=64 chars) with no title
  metadata now use their content as the page heading and breadcrumb label
  (e.g. bitmaps like "12345.bitmap", or short text/name inscriptions).
  Resolution order everywhere: title metadata -> short text content ->
  "#<number>" (heading falls back to "Inscription <number>").

Breadcrumb reinscription dropdowns:
- When a breadcrumb crumb's sat carries other inscriptions (reinscriptions),
  show a caret that opens a dropdown listing them, each linking to its page
  and labelled by the same title/text/number rule. Excludes the crumb's own
  inscription. Walked server-side via get_inscription_ids_by_sat, capped.
- Bold the current (last) crumb; add spacing above the breadcrumb block.

Mobile polish:
- Copy buttons stay inline with truncated ids (the id-truncation now reserves
  the button's width); ids never wrap to a new line.
- On the gallery item page only, shrink the artwork and nav chevrons on small
  viewports so the always-visible arrows fit; the normal inscription page
  stays full-width.

Tests:
- Generalise the heading/breadcrumb tests to text content; add dropdown and
  text-title coverage; the #N breadcrumb fallback test now uses a non-text
  (image) ancestor.
Replace the ▾ glyph (which sits low due to font metrics) with a CSS
border-triangle so the dropdown caret is vertically centered on the line
and a touch larger.
The CSS-triangle caret inherited the button's default colour (black,
invisible on dark themes) and kept the button's default bottom border,
which rendered a second triangle (bowtie shape). Set color: inherit so the
caret uses the breadcrumb text colour, and zero the bottom border.
…anup

* Marketplace dropdown on gallery inscription pages — off-chain data
  layer (static/marketplaces.js) keyed by inscription id, with
  per-marketplace slugs so the same collection can use different slugs
  across Satflow / Ordnet / Ordinals Wallet.
* Generalised .title-dropdown primitive: any title icon with 2+ links
  auto-renders as icon + caret + menu (uniformly for marketplace,
  website, X).
* Breadcrumb dropdowns now also list up to 20 of each parent crumb's
  direct children below the existing reinscriptions (label resolved via
  the same title -> text-title -> #number chain), with an "all children"
  link when there are more than 20. Crumb struct extended; current
  crumb skipped since its children are already in the page body.
* Make dark the default theme and drop the `ord` scheme from the toggle
  cycle (upstream CSS kept intact; only theme-init.js + index.js
  diverge), for visual differentiation from ordinals.com.
* Repoint the nav museum icon at the MoBA parent inscription (regtest
  host is hardcoded for now; to be made network-aware later).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rescue inscriptions inscribed with a wrong `content_type` field (most
commonly `application/octet-stream` for what is actually a glTF, PNG,
MP4, etc.) by inspecting the first 4 KiB of the body on
`/preview/<id>`. Recognised formats: glTF/GLB, glTF+json, PNG, JPEG,
GIF, WebP, MP4 (brand-filtered to exclude HEIC/HEIF/QuickTime), Ogg,
WAV, FLAC, and PDF. Brotli-encoded bodies are transparently decoded
behind a 4 KiB cap.

Only invoked when the stored `content_type` would otherwise resolve to
`Media::Unknown`; correctly-tagged inscriptions are routed exactly as
before. `/content/<id>`, `/embed/<id>`, thumbnails, and oEmbed are
unaffected. See docs/src/inscriptions/rendering.md § Content-Type
Fallback.
When an inscription has multiple parents producing multiple breadcrumb
trails, collapse the longest common prefix and common suffix of those
trails into single inline segments, and render only the divergent middle
of each trail as a separate row. Single-trail breadcrumbs render
byte-equivalently to before.

  before:  Satland / MoBA / Inscription Clubs / Sub 1k / Ordinal Archaeology
           Satland / MoBA / Library / Ordinal Archaeology

  after:   Satland / MoBA / [Inscription Clubs / Sub 1k] / Ordinal Archaeology
                            [Library                    ]

- `BreadcrumbLayout` view helper on `InscriptionHtml` derives
  `prefix` / `middles` / `suffix` from `self.breadcrumbs`. Trails are
  deduped by id sequence first; the prefix is capped at min_len - 1 so
  the current crumb is always reserved for the suffix.
- `templates/inscription.html` rewritten to render `prefix` inline,
  then an optional `.breadcrumb-fork` (one `.breadcrumb-fork-row` per
  trail), then `suffix` inline. Each non-empty row starts with `/`;
  empty middles render as a blank row.
- `static/index.css`: `.breadcrumb` becomes a flex row with
  `align-items: center`, plus `.breadcrumb-fork` (inline-flex column)
  and `.breadcrumb-fork-row` (min-height: 1.2em so blank rows still
  reserve a line of vertical space).
- Tests added for the multi-trail fork and the empty-middle case.

Plus light-theme tweaks: drop the `--link: #f2a900` override (links use
the regular blue in light theme), and swap the light-theme iframe
border rule for a lighter `.thumbnails > a` border.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The header's home link now reads "Ordinals" on mainnet and
"Ordinals.Gallery" elsewhere (regtest, signet, testnet); the small
superscript reads "Gallery" on mainnet (was "beta") and the chain name
otherwise. Keeps the ordinals.gallery branding subtle on a mainnet build
while staying prominent on the gallery's regtest dev environment.

- `templates.rs`: new `home_text()` helper; `superscript()` returns
  "Gallery" on mainnet instead of "beta", chain name elsewhere.
- `page.html`: nav home link calls `self.home_text()` instead of the
  hardcoded "Ordinals•Gallery".
- `server.rs`: regtest test updated to expect the new label.
… scroll

Progressively enhances the server-rendered flat .breadcrumb-fork in three
ways. The Rust `BreadcrumbLayout` and the template stay unchanged; this is
JS + CSS only, so the breadcrumb still renders correctly without JS — it
just shows the flat fork. Zero Rust divergence.

(1) NESTED TREE
    When multiple trails share a leading intermediate crumb (e.g. several
    paths through the same "Inscription Clubs" parent), collapse them into
    one row containing the shared crumb + a sub-fork holding the divergent
    tails. Recurses for deeper sharing, so a 3+ level shared ancestry
    nests cleanly. An inline <svg> branch on the LEFT of every fork level
    draws stem + per-row arms as one continuous shape (single <path>, no
    glyph gaps). Arm y-positions are measured against actual row positions
    so the arms still line up when a branch row is taller than its
    sibling leaf rows.

(2) POPOUT DROPDOWNS
    The .breadcrumbs container uses overflow-x: auto (see (3)), which
    establishes a clipping context that vertically clips any
    .crumb-menu when it opens. Each menu is moved to document.body and
    repositioned `position: fixed` at its toggle's viewport rect when
    opened. Position is viewport-clamped — menus near the left edge drop
    down-right, near the right drop down-left, neither overflows the
    viewport edge. Any scroll (page or breadcrumbs) closes the menu so
    it never floats around detached from its toggle.

(3) HORIZONTAL SCROLL + AFFORDANCE FADE
    `.breadcrumbs` is now `overflow-x: auto` with `padding-bottom` for the
    scrollbar and `.breadcrumb` is `flex-wrap: nowrap; width: max-content`,
    so the breadcrumb keeps its desktop structure on mobile and scrolls
    horizontally instead of wrapping onto multiple lines. A `mask-image`
    gradient fades the right edge when there's more to scroll, and JS
    extends it to fade the left edge once scrolled past the start.

`index.js`: a single IIFE inside the existing DOMContentLoaded handler
that replaces the old `.crumb-toggle` block. ~180 lines, heavily
commented.

`index.css`: updated `.breadcrumbs` / `.breadcrumb` / `.breadcrumb-fork-row`
for nowrap + scroll, plus new `.breadcrumb-tree-group` and
`.breadcrumb-tree-branch` rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NAV REORDER: group the ord-gallery-specific nav icons on the right of the
bar so they're immediately distinguishable from the upstream-ord icons,
with the museum icon as the rightmost. Upstream icons (inscriptions,
runes, collections, blocks, clock, rare, handbook, github, discord) keep
their original relative order; the three gallery additions (galleries,
testnet, museum) move from being interleaved among them to a contiguous
group at the end, before the theme-toggle.

THUMBNAILS: drop the `border-radius: 2%` and `border: 1px solid ordinals#222`
from `.thumbnails > a` (and the matching hover / light-theme
border-color overrides), so thumbnails render as plain square content
with no surrounding frame. The grey outline was visually noisy in light
mode and inconsistent against thumbnails with different background
colours.
Drop the depth==0 special-case in `breadcrumb_trails` so every crumb in
the trail — including the current inscription — has its `children` and
`more_children` populated, not just the parent crumbs.

Initially I skipped children on the current crumb because the page body
already lists them, but that left the breadcrumb trail visually
asymmetric: the dropdown caret would appear on every parent but never
on the current. The dropdown is a navigation shortcut (jump to a child
without scrolling), and there's no real reason it should be absent on
the current crumb.
NAV: move the galleries icon back to its upstream position (right after
collections) — `/galleries` exists in upstream ord too, so it should
keep its original placement. Only the gallery-specific testnet + museum
icons stay grouped at the right end.

PREVIEW IMAGE: change the hardcoded `html { background-color: #131516 }`
in preview-image.html to `transparent`. When an inscription image isn't
square, `background-size: contain` leaves a band of unfilled space on
either top/bottom or left/right; with a hardcoded dark bg, that band
shows as a dark "border" against light-themed parent pages. Transparent
lets the iframe blend with whatever the embedding page renders.
Previously the breadcrumb was suppressed for inscriptions with no
parents, which meant root inscriptions (e.g. Satland) had no
breadcrumb at all. That removed an otherwise useful navigation entry
point — the current crumb's dropdown is now where you click to jump
into its children, and a root inscription is exactly where that
navigation pattern is most useful.

Drop the `if info.parents.is_empty()` short-circuit and always call
`breadcrumb_trails`. For a root inscription this yields a single trail
containing just the current crumb, which the template renders as the
bold current name with a `▾` for the children dropdown.
`nav a.active { color: #ffffff }` was hardcoded white, which made the
home logo invisible against the white nav background in light mode (the
home link picks up .active when window.location.pathname is "/"). Use
var(--light-fg) so it stays white in dark mode and switches to black in
light mode.
Previous rule rendered a single-crumb breadcrumb even for isolated leaf
inscriptions, which is just visual clutter (the dropdown would be empty
too). Tighten to: render the trail only when there is somewhere to
navigate to — i.e. at least one parent or at least one child.
Grid thumbnails for image media now emit <img src=/content/...> instead of
a sandboxed iframe loading /preview: browsers cache and persist images
across scrolling, where lazy iframes are discarded offscreen and reload
on re-entry. Non-image media (HTML, scripted SVG, video, models) keep the
sandboxed iframe. Media kind is resolved server-side via sniffed_media()
and threaded to templates only on HTML responses; JSON API unchanged.
Grid pages ship Content-Security-Policy: default-src 'self', which
silently discards style attributes, so img thumbnails lost their
pixelated rendering. Carry the rendering hint as a class and style it
from the stylesheet instead.
- /inscriptions?cursed=1 shows only cursed inscriptions via a dedicated
  index query over INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER (dense negative
  numbering allows arithmetic pagination); both sorts supported and the
  toggle auto-submits and round-trips through pagination links
- hide the not-yet-functional Range and Satributes controls
- museum nav icon now routes to a local /museum Coming Soon page
- custom select chevron (static asset, CSP-safe) with even padding
Preview pages had no viewport meta, so iOS Safari laid them out at the
legacy 980px virtual width and rescaled: image previews lost their
pixelated rendering to fractional downscaling, and iframe-heavy grid
pages ballooned each cell to a 980px-wide backing store — enough to
trip Safari's memory ceiling and kill /galleries outright. Upstream
has the same bug; the fork's embed templates were already fixed.
iOS Safari ignores image-rendering on scaled background-images (GPU
compositing smooths regardless) while honoring it on img elements —
pixel art previews rendered blurry on iPhone while the img-based grid
thumbnails were crisp on the same device. Keep the downscale-to-auto
logic, retargeted at the img.
Offscreen grid cells now skip layout, paint, and memory entirely —
decoded images are discarded and lazy iframes are never instantiated
until scrolled near. Keeps iframe-heavy pages (/galleries) inside
mobile renderer memory limits.
aspect-ratio on the img fought height:100% inside gallery-row flex
cells — iOS resolved the conflict by the image's natural ratio,
stretching gallery items tall. Constrain the anchor cell instead.
natebitsats and others added 6 commits July 27, 2026 09:21
Grid thumbnails are inert (pointer-events: none), yet every HTML
inscription cell ran arbitrary scripts — a grid of live apps sustains
enough CPU that iOS's 50%-over-600s watchdog kills the renderer
(confirmed via device crash report; two covers ran render loops for
the page's entire life). Drop allow-scripts for Media::Iframe cells
only; our own preview wrappers still script, full previews unchanged.
Script-less HTML thumbnails laid out at cell size crop their static
DOM badly. Lay the embedded page out at 4x the cell (~a real page
width) and transform-scale it to fit — static covers read as a
miniature of the actual page.
Percentage sizing on the scaled iframes resolved to zero width inside
the gallery-row flex track; absolute positioning resolves against the
cell box in every layout context.
acme_domains() falls back to System::host_name() when --acme-domain is
unset, and ServerConfig.domain was populated from it, so og:image on
every page advertised the node's local hostname.

Add --domain, resolve it via public_domain() which only ever returns an
explicitly configured domain, and let og_image() delegate to
page_origin() so csp_origin takes precedence. Instances already passing
--csp-origin are fixed without new configuration.

Authored 2026-07-28 and left uncommitted; committed unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcTzFwriTBkUPg6QvYymo7
Thumbnails for HTML and SVG inscriptions carried a valueless `sandbox`,
which blocks scripts. Recursive art builds itself at runtime, so every
such thumbnail rendered blank — a 111-item gallery of recursive HTML
showed an empty strip. index.js contradicted this too, giving lazily
loaded thumbnails allow-scripts, so a gallery past its first page mixed
dead and live tiles.

Mark these frames data-scriptable and let index.js grant allow-scripts
to on-screen thumbnails, revoking it as they leave and holding at most
MAX_LIVE_THUMBNAILS at once — keeping the CPU ceiling that made them
inert. Sandbox flags only apply on navigation, so each transition swaps
in a fresh clone; allow-same-origin is never granted. Visibility carries
across the swap, otherwise eviction frees a slot that the next observer
callback immediately refills, churning forever.

The inert markup stays the no-JS floor: SVG paints without scripts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcTzFwriTBkUPg6QvYymo7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant