Skip to content

WEBDEV-8458 Migrate item-navigator into elements - #64

Open
iisa wants to merge 31 commits into
mainfrom
WEBDEV-8458-migrate-item-navigator
Open

WEBDEV-8458 Migrate item-navigator into elements#64
iisa wants to merge 31 commits into
mainfrom
WEBDEV-8458-migrate-item-navigator

Conversation

@iisa

@iisa iisa commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

WEBDEV-8458 Migrate item-navigator into elements

Ports @internetarchive/iaux-item-navigator into elements as ia-item-navigator, re-styled to the elements CSS conventions, with a live demo and full test coverage.

What ships

A fullscreen-capable shell — header/main slots, a drawer driven by a menuContents provider array, a minimized menuShortcuts rail, overlay/shift at 600px — plus its internals (ia-itemnav-menu-slider, -menu-button, -no-theater-available) and two bundled menus: -viewable-files-panel with -sort-files-button (volume list, active-file highlight, PDF flagging, tri-state sort) and -share-panel (social links, copyable iframe/bbcode embeds).

Lives at src/elements/ia-item-navigator/, exported from src/elements/index.ts.

Key decisions

  • Sub-elements are namespaced under ia-itemnav- — tags, files and classes. Not cosmetic: upstream registers ia-menu-slider, ia-itemnav-loader and ia-no-theater-available verbatim, and elements re-exports the navigator from its root barrel — so any @internetarchive/elements import would register those twice and throw NotSupportedError at module evaluation while consumers still load the old package.
  • Icons are assets, not bundle. 14 standalone .svg files — ~18KB of path data out of the JS bundle — drawn through a maskedIcon() helper so they stay recolorable.
  • No dependencies added. Not one. The navigator now takes a dependency only where it constructs or calls something, and neither of the two it inherited survived that test — see below.
  • The loading spinner is gone. loaded still gates the reader; an indicator returns on WEBDEV-8951.
  • Sizing anchors to a self-contained 10px base in em, matching petabox's scale without forcing a 10px root on the demo.

Dependencies: both inherited ones removed

Reviewing the two type-shaped dependencies turned up the same answer twice, for different reasons.

@internetarchive/metadata-service is gone. It sat in dependencies, so every consumer of this package installed it — plus field-parsers, iaux-item-metadata, result-type and typescript-memoize — whether or not they touched the navigator. It was used for exactly one field, item.metadata.identifier, which is the one field upstream deliberately leaves unwrapped because it can only ever be a string. So item is now identifier: string.

The property is renamed, not just retyped, so a consumer still passing an object gets an unknown property and fails visibly rather than quietly supplying a shape the navigator can't read. It also matches the child element, which already declared identifier.

The shared resize observer is gone too — the whole concept, not just the type. The navigator required a host-injected observer for two things, and neither needed JavaScript. Details below.

Panel open/close semantics

The navigator owns whether the drawer and a panel are open. The slider reports what the user did and renders what comes back — one copy of the state, in one place.

That makes each surface mean what it says:

  • Closed means closed. A closed panel is inert, so tab order and the accessibility tree match the screen, and aria-expanded reports what's actually open.
  • Focus follows state — into a surface when it opens, back to whatever opened it when it closes, including from the shortcut rail.
  • Closing the drawer closes its panel, so nothing stale reappears on the next open.
  • Opening straight to a panel is one movement — the drawer carries the panel in rather than both sliding.
  • The scroll reacts to the file list or selection changing, so it waits on nothing. That also covers two cases it previously missed: bookreader reuses one panel element, so firstUpdated fired only on the first open; offshoot reuses it on a file click, so the active row could move without the panel following.

None of this was modelled before. The navigator held menuOpened/openMenu while the slider held its own selectedMenu, plus open and animateMenuOpen. Since menuTypeSelected is composed, selecting a channel toggled both copies independently and the navigator then overwrote the slider's — they agreed only because the two toggle expressions happened to match. A static attribute set slider.open once, so it stayed true forever; nothing ever set animateMenuOpen, so .animate never fired. With no single fact to react to, the scroll had to guess when layout would settle — which is where a 350ms timer came from.

Transitions themselves didn't change; they follow the state flip now instead of being something to schedule around.

Layout drives the breakpoint

The host used to inject a shared resize observer. It fed two things:

The overlay/shift breakpoint is a container query now. It keys off the frame's own width exactly as before — a narrow navigator inside a wide page still overlays — but the browser evaluates it during layout rather than in a callback. openMenuState and the overlay/shift classes are gone; shift is the default and the query overrides it.

The header-slot registration was worse than redundant. It re-rendered so render() could measure the slotted header's offsetHeight and write it back as an inline height. offsetHeight excludes margins, so a header with vertical margins was pinned shorter than its own box and its bottom margin overlapped the reader — clipped rather than visibly broken, since the frame hides overflow. Removing the measurement lets the slot size itself, margins included, which fixes that bug.

Removing the registrations also removed a leak they carried: the slot's handler was an anonymous object built fresh on every call, so nothing could ever remove it. It survived both disconnect and observer swaps, holding a detached element against the host's long-lived singleton.

sharedObserver and service-interfaces.ts go with it, so hosts supply nothing. Consumers still setting the property are unaffected — an unknown property on a Lit element is a no-op.

Theming

17 public knobs, each with a private --x-- alias and an inline default. All prefixed --item-navigator-:

  • Sizingbase-font-size 10px · menu-width 320px · menu-margin 42px · animation-timing 200ms · icon-size 2.4em · header-icon-size 2em · menu-button-label-display none
  • Colortext-color var(--true-white) · icon-color and icon-active-color → text color · icon-inactive-color var(--lighter-gray) · border-color #4b4b4b · active-file-border-color #538bc5
  • Surfacestheater-bg-color #000 · menu-slider-bg #212121 · active-button-bg var(--mid-gray) · share-embed-bg #151515

Review responses

Ask Resolution
Externalize the icon SVGs ✅ 14 .svg files, ~18KB of path data out of the bundle
Scope element names to the component ia-itemnav-* across tags, files, classes
350ms scroll timeout feels code-smelly ✅ symptom, not cause — panel state drives it now
Does render() need the html wrapper? ✅ no — signature widened, branch returns directly
CustomTheaterInterface overreaches; how does it meet offshoot's <ia-theater>? ✅ deleted — dead code, already dead upstream; unrelated to offshoot's theater
What happens with the actual ModalManager? ✅ nothing — never read, so removed
What happens with the actual SharedResizeObserver? ✅ moot — the navigator no longer uses one
Import the resize-observer type from its package ✅ moot — same reason; service-interfaces.ts is deleted
How does ia-no-theater-available meet offshoot's? ⬜ deferred — legacy from the port, still live in petabox

Demo & QA

Demo: https://internetarchive.github.io/elements/pr/pr-64/#elem-ia-item-navigator — toggles under Settings, theming under Import, Usage & Settings.

Shell — theater + header render; Header off hides the bar; View available off shows the placeholder with the right identifier; Loaded off leaves the frame empty (no spinner); Fullscreen fills the viewport with an in-frame Exit.

Drawer and panels

  • ⋯ opens/closes the drawer; while closed, Tab doesn't enter it.
  • A rail shortcut opens straight to that panel as one movement, not two slides.
  • With the drawer open, clicking between panels swaps them; only the empty↔panel edges animate.
  • ✕ closes the panel and leaves the drawer open; Esc closes panel first, then drawer.
  • Focus lands in a panel on open and returns to the control that opened it on close.
  • A closed panel hides completely — nothing behind the left-flush icons — and closing the drawer clears it, so reopening shows no stale panel.

Menus

  • Viewable Files: titles wrap, PDF tagged, active file highlighted and scrolled into view; sort cycles neutral→asc→desc; selecting a file swaps the theater.
  • Share: links open real URLs; embed field copies and flashes "Copied".

Responsive — narrow the browser: above 600px the theater eases in sync with the drawer and stays flush to its edge; below 600px the drawer overlays a full-width theater. Because this is a container query it tracks the component's width, so resizing the demo frame works as well as resizing the window.

Header — a slotted header with vertical margins should sit fully above the theater, with no clipping where the two meet.

Theming — the four sliders and ten color pickers update live and name the var each sets; base font size scales the whole component, icons included (evidence the aliases are wired, not just declared); palettes apply and Reset restores defaults.

Animation off — the check that matters most here. Set Animate off (--item-navigator-animation-timing: 0ms) and repeat the drawer-and-panel checks. Everything holds, which is the evidence nothing times against the transition.

Automatedpnpm test (110 navigator tests of 304), pnpm run build and the madge circular check pass. The navigator covers 100% of lines and functions, 97% of branches.

Follow-ups (tracked, out of scope)

  • WEBDEV-8796 / #66 — shared dark-surface tokens for the near-blacks, border and scrim with no house equivalent.
  • WEBDEV-8951 — offshoot's status indicators into elements; the loading indicator returns on top of it.
  • WEBDEV-8952 — swap bookreader to the elements navigator.

Consumer note

Two breaking changes land here, both free while the component is unreleased, and both already on the migration tickets:

  • itemidentifier, and the two consumers need different fixes. bookreader holds a MetadataResponse and wants .metadata.identifier; offshoot passes public_metadata, a Metadata, and wants .identifier directly — which is a live bug today, since .metadata is absent there and the placeholder never receives an identifier.
  • sharedObserver is no longer read. Consumers can drop the binding; leaving it in place is harmless.

iisa and others added 3 commits July 20, 2026 12:50
Port @internetarchive/iaux-item-navigator into src/labs/ia-item-navigator,
adopting the elements CSS convention, with a demo and tests.

Scope: the navigator shell (ia-item-navigator + ia-menu-slider,
ia-menu-button, ia-itemnav-loader, ia-no-theater-available) and the
viewable-files menu (ia-viewable-files-panel + ia-sort-files-button). The
bundled social share panel is left as a follow-up.

- Rename to the ia-* convention (tag ia-item-navigator, class IAItemNavigator);
  sub-elements prefixed likewise.
- Loosen deps: modal-manager and shared-resize-observer were type-only imports,
  now local interfaces (interfaces/service-interfaces.ts) — zero runtime dep.
  Keep @internetarchive/metadata-service (real runtime use in the item
  converter). Inline all icons (ellipses, collapse-sidebar, viewable-files,
  sort) instead of adding icon-package deps.
- CSS convention: public theming vars become --item-navigator-* with --x--
  private aliases and inline defaults; themeStyles imported per component;
  palette tokens replace hardcoded colors where they map.
- Fix a latent toggle bug: the menu button bound @click=${this.toggleMenu},
  passing the event as forceValue; now wrapped so menuOpened stays boolean.
- ia-sort-files-button sorts a copy rather than mutating the reactive prop
  in place; sort icons use currentColor so they theme with the icon fill var.
- Demo story (labs, auto-discovered) wires the real viewable-files panel as a
  menu provider with the sort button as its action; live style settings.
- Vitest browser-mode tests (42) covering drawer, provider rendering,
  responsive mode, no-theater state, menu selection, file listing, PDF
  flagging, URL building and sort cycling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complete the migration by porting the social share panel (from
iaux-in-share-panel) as ia-share-panel, so the item-navigator's bundled menus
are fully migrated.

- Rename to ia-share-panel / IASharePanel; export shareIcon as the provider
  icon.
- Inline all 7 social/link icons (share, twitter, facebook, tumblr, pinterest,
  email, link) as .fill-color templates in menus/share-icons.ts — no
  icon-package deps.
- Upgrade the deprecated document.execCommand('copy') to the async Clipboard
  API, falling back to execCommand only where the async API is unavailable;
  track the copy-note timeout per note via a WeakMap.
- CSS convention: theming vars become --item-navigator-* with --x-- aliases and
  inline defaults; base-font-family from themeStyles replaces the hardcoded
  font stack.
- Wire the share panel into the demo as a menu provider.
- 5 Vitest tests: option population, share-URL building, embed snippets,
  header toggle, and the copy-note flash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move src/labs/ia-item-navigator to src/elements/ia-item-navigator now that the
migration is complete, treat it as a production-ready component:

- Drop the `labs` flag from the story so it lists under Production-Ready and
  its import/usage snippets use the @internetarchive/elements/ia-item-navigator
  path (no /labs segment).
- Export the navigator and its consumer-facing menus (viewable-files panel,
  sort-files button, share panel) from src/elements/index.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

🚀 View preview at
https://internetarchive.github.io/elements/pr/pr-64/

Built to branch ghpages at 2026-08-27 23:53 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@codecov-commenter

codecov-commenter commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.67797% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.60%. Comparing base (8c054a6) to head (30de5d1).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ments/ia-item-navigator/ia-item-navigator-story.ts 62.00% 13 Missing and 6 partials ⚠️
demo/story-components/story-styles-settings.ts 73.52% 0 Missing and 9 partials ⚠️
...ements/ia-item-navigator/ia-itemnav-menu-slider.ts 94.44% 0 Missing and 3 partials ⚠️
demo/story-template.ts 0.00% 1 Missing ⚠️
...rc/elements/ia-item-navigator/ia-item-navigator.ts 98.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #64      +/-   ##
==========================================
+ Coverage   81.12%   84.60%   +3.48%     
==========================================
  Files          27       38      +11     
  Lines         927     1273     +346     
  Branches      227      303      +76     
==========================================
+ Hits          752     1077     +325     
- Misses        113      115       +2     
- Partials       62       81      +19     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

iisa and others added 7 commits July 20, 2026 13:34
Fold the useful examples from the upstream demo into the story:

- Realistic multi-file item (the "Master Book of American Folk Song" volumes)
  for the viewable-files panel, including a deliberately long title (wrapping)
  and PDF entries (PDF flag), with one active file.
- A slotted header bar (brand + item title link) and a styled theater mock in
  slot="main", so the shell's host-content slots are shown in use.
- A populated minimized shortcut rail (menuShortcuts) alongside the drawer.
- Header, view-available, and fullscreen toggles; fullscreen renders an
  in-frame "Exit fullscreen" control so the demo can't trap the viewer.
- Size shortcut-rail icons consistently with the toggle button, and give
  viewableFilesIcon the shared .ia-icon class so it themes/sizes like the rest.

Kept self-contained: no metadata-service fetch or external image hotlinks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The components' internal sizing was in rem, calibrated for petabox's 10px root
font-size (which the upstream demo set on the document). In the elements demo
(default 16px root) everything — icons especially — rendered ~1.6x too large.

Make the navigator self-contained instead of depending on the consumer's root:

- Set font-size: var(--item-navigator-base-font-size, 10px) on each component's
  :host and convert internal rem units to em, so sizing resolves against the
  component's own base. Consumers can override the var to rescale everything.
- Normalize buttons with `font: inherit` (and reset the header/social icon
  wrappers), since form controls don't inherit font-size — otherwise em icons
  resolved against the browser's default button font-size.
- Move menu-button text sizing off the flex container onto the label so the
  icon box isn't compounded.

Verified in the demo: toggle/shortcut icons 24px, menu icon box 42px, labels
16px, close/sort/share icons ~20px — matching the upstream 10px-base scale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The provider icons inside the open drawer weren't explicitly sized, so they
filled the 42px icon box instead of matching the 24px minimized-rail shortcuts.
Size the glyph within the menu-button icon box to --item-navigator-icon-width--
(the same var the rail uses) and drop the hardcoded 100% inline size on the
demo's placeholder icon so CSS controls it. Drawer icons now render 24px, in
line with the shortcuts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Animate the outer drawer open/close via transform + `inert` (instead of a
  display:none toggle, which can't transition), matching the inner panel.
- Expose the minimized sidebar rail as `part="minimized-menu"` (upstream #27).
- Close the menu sub-panel independently of the drawer: the slider emits
  `menuPanelClosed` and the navigator clears `openMenu` without closing the
  drawer, so a channel can be reopened afterward (fixes an openMenu desync).
- Ease the reader's width/margin in shift mode so the slotted theater glides
  in sync with the drawer and settles smoothly on resize; overlay stays instant.
- Add tests for the independent sub-panel close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Enable menu-button labels in the demo (component ships icon-only by default).
- Point demo data at the real `masterbookofamericanfolksong00shep` item so the
  viewable-file and share links resolve; keep PDF flags via file_source.
- Bind the settings checkboxes with `.checked` so exiting fullscreen from the
  theater unchecks the Fullscreen box.
- Add an "Animate" toggle that enables/disables all navigator animations via
  --item-navigator-animation-timing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Fullscreen: the base `#frame { height: inherit }` overrode `top/bottom: 0`
  on the fixed element, so fullscreen was stuck at the wrapper height. Release
  height (height: auto; min-height: 0) so the inset fills the viewport.
- Slider header: `selectedMenuAction` defaults to Lit's `nothing` sentinel,
  which is truthy — so renderMenuHeader always applied `with-secondary-action`
  and an empty action span. Compare against `nothing` explicitly.
- Add tests covering the previously-uncovered paths (item attribute converter,
  shared-resize-observer wiring, menu-event handlers, shortcut rail, clipboard
  fallback + copy-note timeout, sort comparators, viewable-files scroll
  branches, slider focus restoration). All runtime component files are now at
  100% statements/branches/functions/lines; 76 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…imation

The minimized shortcut rail floats over the frame's left edge while the drawer
is closed. Previously the reader reserved that width with a margin, so on a
shift-mode open it animated from 42px to the drawer width while the drawer slid
from 0 — leaving a shrinking gap so the theater appeared to trail the drawer.

Reserve the rail's width with non-transitioned padding on the reader's content
(only while closed) instead. The reader box now animates 0 -> drawer width in
lockstep with the drawer (identical timing/easing), and the padding snaps away
on open, so the theater stays flush with the drawer through the slide while
still clearing the rail when closed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Theming:
- Merge subpanel/separator/share borders into --item-navigator-border-color
- Collapse all text onto --item-navigator-text-color
- Merge icon knobs into --item-navigator-icon-color / -active-color / -inactive-color
- Rename --item-navigator-textarea-bg -> --item-navigator-share-embed-bg
- Drop no-op --item-navigator-share-icon-bg; remove dead loader fill=#333

Demo:
- Styles panel shows the CSS var each control sets, plus a Reset button
- List all theming/layout toggles; empty apply reverts to component defaults
- Realistic archive.org cat items; clicking a viewable file swaps the theater iframe

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/elements/ia-item-navigator/icons.ts Outdated
d="m9 0c4.9705627 0 9 4.02943725 9 9 0 4.9705627-4.0294373 9-9 9-4.97056275 0-9-4.0294373-9-9 0-4.97056275 4.02943725-9 9-9zm1.6976167 5.28352881c-.365258-.3556459-.9328083-.37581056-1.32099801-.06558269l-.09308988.0844372-3 3.08108108-.08194436.09533317c-.27484337.36339327-.26799482.87009349.01656959 1.22592581l.084491.09308363 3 2.91891889.09533796.0818904c.3633964.2746544.8699472.2677153 1.2256839-.0167901l.093059-.0844712.0818904-.095338c.2746544-.3633964.2677153-.8699472-.0167901-1.2256839l-.0844712-.093059-2.283355-2.2222741 2.3024712-2.36338332.0819252-.09530804c.2997677-.39632298.2644782-.96313393-.1007797-1.31877983z"
fill-rule="evenodd"
/>
</svg>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move these into standalone svg files so we're not packaging them as part of the JS bundle?

@iisa iisa Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved them to standalone .svg files — rendered with mask-image so they stay recolorable.

Comment thread src/elements/ia-item-navigator/ia-itemnav-no-theater-available.ts
/** Handler invoked by a shared resize observer when its target resizes. */
export interface SharedResizeObserverResizeHandlerInterface {
handleResize(entry: ResizeObserverEntry): void;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we import this from @internetarchive/shared-resize-observer instead of re-creating it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally!

Comment thread src/elements/ia-item-navigator/interfaces/service-interfaces.ts Outdated
Comment thread src/elements/ia-item-navigator/menus/ia-viewable-files-panel.ts Outdated
Comment thread src/elements/ia-item-navigator/menus/ia-viewable-files-panel.ts Outdated
Comment thread src/elements/ia-item-navigator/menus/ia-itemnav-viewable-files-panel.ts Outdated
Comment thread src/elements/ia-item-navigator/menus/ia-viewable-files-panel.ts Outdated
Comment thread src/elements/ia-item-navigator/interfaces/custom-theater-interface.ts Outdated
Comment thread src/elements/ia-item-navigator/ia-item-navigator-story.ts Outdated
@jbuckner

Copy link
Copy Markdown
Collaborator

Can we scope the name of all of the elements, ie ia-itemnav-* since they're going to exist in the global namespace and externalize all of the svgs so they don't inflate the size of the JS bundle?

Comment thread src/elements/ia-item-navigator/ia-item-navigator.ts Outdated
iisa and others added 3 commits August 5, 2026 18:30
The 13 glyphs moved from inline Lit templates to standalone .svg files,
taking ~18KB of path data out of the JS bundle. They render through a
shared maskedIcon() helper using CSS mask-image rather than <img>, so
they stay recolorable by the --item-navigator-icon-color knobs; <img>
would have dropped that theming.

- shareIcon/viewableFilesIcon keep their TemplateResult export shape,
  now rendering a masked <span> instead of inline <svg>
- Host theming switches to background-color, but the original
  .fill-color rules stay so consumer-supplied inline-svg icons still
  theme without changes
- The loader's book/ring stays inline; a mask can't animate its
  spinning sub-path
- Quote the mask url(): bundlers inline small SVGs as data URIs
  containing apostrophes, which an unquoted url() silently rejects
- Demo gains a Randomize colors button to spot any glyph that fails
  to pick up its color knob

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Randomizing each color knob on its own could land dark text on a dark
panel, so the control couldn't be trusted to show whether an element
follows its color knob. It now swaps in one coordinated theme at a
time — Midnight, Forest, Plum, Solarized or High Contrast — each
checked against every foreground/background pair the navigator
actually renders: WCAG AA throughout, AAA for text. Recoloring the
whole component at once also makes a stray element obvious.

Stories opt in by passing palettes alongside their style settings, so
the other six demos keep the existing per-input behavior. The button
now hides where there is nothing to recolor.

Also fixes a latent bug that left the panel unable to re-render at
all: the range readout and reset wrote to <output> with textContent,
ejecting Lit's markers from a node it renders. The readout is now
driven by reactive state instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The style-variable labels, revert control and randomizer went into the
shared settings panel, so they appeared on every component's demo even
though this work only concerns the item navigator. Stories now opt in:
palettes bring the randomizer, revertable brings revert, and
showCssVariables shows each control's custom property. Only the item
navigator opts in, so the other six demos render exactly as before.

Randomizing without palettes assigned each color on its own, which
could pair dark text with a dark panel; with the control now limited
to stories that define themes, that path is gone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
iisa and others added 3 commits August 5, 2026 19:42
Custom elements share one global registry, and names like ia-menu-slider
or ia-share-panel are generic enough that anything on the page might
want them. Three were worse than generic: ia-menu-slider,
ia-itemnav-loader and ia-no-theater-available are registered verbatim by
@internetarchive/ia-item-navigator, which bookreader and offshoot still
load. Since the barrel re-exports this component, importing anything
from elements would have registered those names a second time and thrown
at module evaluation.

The navigator's parts now carry its prefix, with files and classes
following the tags. The loader becomes ia-itemnav-loading-view rather
than ia-itemnav-loader, since the prefixed name is the one upstream
already owns. The navigator itself keeps its name.

Each element now declares its tag in HTMLElementTagNameMap, so
querySelector is typed and a mistyped tag in a template fails the build
instead of silently rendering an inert element. A test asserts the new
names are registered and the old ones are left free, so a half-finished
rename cannot pass quietly.

Doing this before release costs consumers nothing: they reference the
upstream tags today and rewrite those either way when they migrate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
render() wrapped its result in an html template purely to satisfy the
TemplateResult return type; widening the type to include nothing lets
it return the branch as-is, without the extra template.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The navigator accepted a modal property and never read it. Nothing
downstream read it back either: in bookreader the menu providers get
the modal manager directly through baseProviderConfig, a path that
does not go through the navigator, and offshoot never passed one. The
property was vestigial upstream too — iaux-item-navigator declares it
and likewise never uses it.

Removing it also retires the local ModalManagerInterface, leaving
service-interfaces to describe only the resize observer, which the
navigator genuinely uses.

Consumers still binding .modal are unaffected: setting an unknown
property on a Lit element is a no-op, and their providers keep their
own reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@iisa

iisa commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

@jbuckner thanks for the review — pushed a batch addressing most of it. Replies are on the individual threads; summary here.

Addressed

Externalized the SVGs (7962444) — all 13 glyphs are standalone .svg files now, ~18KB of path data out of the JS bundle, verified gone from the emitted dist. They render via a small maskedIcon() helper using mask-image + background-color rather than <img src>: these icons are recolored through --item-navigator-icon-color and the active/inactive states, and an <img> can't be recolored. The original .fill-color rules are kept alongside, so inline-<svg> icons a consumer passes in (bookreader's bookmarks/search/downloads providers) keep theming with no change on their side. The loader's book/ring stays inline — a mask can't animate the spinning sub-path.

Scoped the element names (dceca7a) — sub-elements are now ia-itemnav-*, with files and classes following the tags. This turned out to be more than hygiene: @internetarchive/ia-item-navigator registers ia-menu-slider, ia-itemnav-loader and ia-no-theater-available verbatim, and since we re-export the navigator from the root barrel, any @internetarchive/elements import would have registered those a second time and thrown NotSupportedError at module evaluation while consumers still load the old package. The loader became ia-itemnav-loading-view because the prefixed name is the one upstream already owns.

Each element now declares its tag in HTMLElementTagNameMap, so querySelector is typed and a mistyped tag in a template fails the build instead of silently rendering an inert element — there was no such check before. A test asserts the new names are registered and the old ones left free, so a half-finished rename can't pass quietly. I also added a short naming note to the README, since it only documented directory layout.

Dropped the modal manager (8cba8d4) — the navigator accepted modal and never read it, and nothing read it back out: bookreader's providers get the modal directly via baseProviderConfig, bypassing the navigator. It's unused upstream too. ModalManagerInterface went with it, so service-interfaces now describes only the resize observer — which is load-bearing (600px overlay/shift, header re-measure, cleanup on swap/disconnect), and a real SharedResizeObserver satisfies it as-is.

Removed the redundant html wrapper (6a28604) — you were right, it only existed to satisfy the return type.

Still open

  • Importing the resize-observer type from @internetarchive/shared-resize-observer instead of declaring it locally — agreed, not done yet.
  • The 350ms scroll timeout.
  • Scoping custom-theater-interface (now one field smaller with modal gone) and how it relates to offshoot's <ia-theater>.
  • Why the story uses its own resize-observer adapter — replied on the thread; happy to switch if you'd rather the demo use the real one.

Consumer impact from the renames is tracked in WEBDEV-8856 (bookreader) and WEBDEV-8857 (offshoot), including the ordering constraint that bookreader has to land first — offshoot reaches into its shadow root for the navigator tag.

npm test (273), npm run build and npm run lint all pass.

The panel's open/close was spread across two components, so the code
worked around the animation rather than the behaviour that caused it —
most visibly a 350ms timer standing in for 'the list is ready'.

The navigator now owns whether the drawer and a panel are open, and the
slider reports what the user did and renders what comes back. That
retires a second copy of the selection that only agreed with the first
by coincidence, plus two properties that never did anything: the
slider's own open flag, frozen true by a static attribute, and
animateMenuOpen, which nothing set.

Because the state is now in one place it can be stated in the markup:
aria-expanded reflects whether a surface is actually open rather than
being hardcoded false, the drawer and panel carry roles and names, the
panel is named by its own heading, and closed panels are inert so the
tab order and the accessibility tree agree with the screen. Opening a
surface moves focus into it and closing returns focus to whatever
opened it — including from the shortcut rail, which used to hide the
focused button and drop focus to the document. Closing the drawer also
closes the panel inside it, so a stale panel can't reappear.

The scroll follows the same rule: it runs when the list or the
selection changes, so there is nothing to wait for. Menu buttons also
carry an aria-label, keeping their name from depending on a styling
variable a consumer might not set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
iisa and others added 13 commits August 25, 2026 11:10
Every glyph is square and all eight call sites set width and height
together, so the separate width and height knobs only doubled the API.
They also disagreed: the header icon defaulted to 2em in the slider and
18px in the sort button, and both render in the same panel header — the
close icon came out 20px next to an 18px sort icon.

One knob per icon group can only carry one default, so the two can no
longer drift. The header settles on 2em, which the close buttons
already used and which follows the em-against-10px sizing the rest of
the component uses.

That exposed why the sort button had been pinned to a pixel value:
buttons don't inherit font-size, so it was sizing its glyph against the
UA default rather than the component base. It now resets font the same
way the close button beside it does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each component spelled out var(--item-navigator-base-font-size, 10px)
where it needed the base, so the default lived in ten places. Every
other knob in the component declares a private alias once and reads
that, which is what keeps a default from drifting between files — the
habit that was missing when the header icon knob ended up 2em in the
slider and 18px in the sort button.

Same public name, same 10px default, no visual change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opening straight to a panel from a shortcut looked a beat slow. The
panel is nested inside the drawer, so their transforms compose: the
panel covered 640px in the time the drawer covered 320px, staying
clipped for the first quarter of the animation and then arriving at
double speed. Opening the same panel from a menu button was fine
because the drawer is already in place and the panel travels its own
320px.

Opening straight to a panel is one movement, so the panel now holds
still and lets the drawer carry it in. CSS cannot tell the two apart on
its own — both paths end that frame with the drawer and the panel open
— so openShortcut marks the render where the drawer was actually
closed, and the navigator hands the slider a transition value through
an inherited custom property.

Every other path clears the mark, rather than waiting on the drawer's
transitionend: a zero-duration transition never fires one, so with
animations turned off the flag would have stranded on and suppressed
the panel's animation for good.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every menu icon sat at z-index 2, above the panel at z-index 1, so
opening or closing a panel showed its contents travelling behind the
icon column — the inactive icons have no background of their own, so
there was nothing to hide it.

Only the open entry's icon needs to be above: it borrows the panel's
background and rounds into it, and the two have to read as one shape.
The rest now sit below the panel and are simply covered as it moves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The panel's movement had no tests, so the recent fixes to it rested on
manual checks. These pin the vocabulary: the panel slides on the edges
between empty and open, switching between panels swaps contents without
re-running the slide, and the host can suppress the slide so the drawer
carries the panel in.

Also covers the marker's lifecycle — set only when the drawer was
actually closed, and cleared by selecting another channel, closing the
panel or closing the drawer — and that only the open entry's icon lifts
above the panel, which is what kept the panel from sliding visibly
behind the others.

Positions are read with animations disabled, since a computed transform
mid-transition reports wherever the animation has reached rather than
where it is headed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CustomTheaterInterface described a host that feeds menu providers and
shortcuts to the navigator, but nothing has ever been that host. It had
a single reference in the repo — its own declaration — and no
implementer, importer or consumer in elements, offshoot, bookreader or
petabox. It was already dead in iaux-item-navigator and came across
verbatim when the component moved out of labs.

Eight of its twelve members appear nowhere in the navigator; the four
that do are properties the navigator declares on itself, sharing only a
name. Its real seam with a theater is the header and main slots plus
the shortcuts event, which stay deliberately untyped because the
navigator is a shell.

An interface nothing implements cannot be checked, so it drifts: this
one carried a modal member until that turned out to be unused too.

Also drops a binding in the slider tests that stopped being read when
those assertions moved to the rendered box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The demo drew every viewable file from a pile of cat items, which
covered images, PDFs and a video but left the navigator's other
theaters unshown. It now runs on Dolly Parton across six real,
public items: two images, a video, an album, a scanned book and a
magazine PDF.

The album and the book are lending items, so they embed as samples
and preview pages rather than the whole work — which is what a
consumer hits in production, and worth having in the demo for that
reason.

Media types grow by two to carry audio and book; both take the
standard embed path, so only PDFs still load their file directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tem-navigator

# Conflicts:
#	package-lock.json
The navigator is a shell that doesn't know what it is hosting, so it
shouldn't decide what loading looks like either. It shipped an animated
book-and-ring glyph and, in fullscreen, an 'Internet Archive' caption —
branding and motion a consumer may already have its own version of, with
no way to replace or suppress it.

The loaded property stays and still gates the reader, so consumers keep
the hook they use today; what changes is that the frame is simply empty
until loaded rather than showing our spinner. A consumer that wants an
indicator can slot its own, which is the same seam the theater already
uses.

This also retires the last inline SVG in the component — it stayed
inline through the mask-image migration because a mask can't animate the
spinning ring on its own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Folds the panel-behaviour branch into the migration so the work reviews as one PR: panel state ownership, the icon-size and base-font-size knob consolidation, the panel-movement tests, the unimplemented theater contract removal, the Dolly Parton demo, and the loading spinner removal.
The navigator carried @internetarchive/metadata-service as a runtime
dependency, so every consumer of this package installed it — along with
field-parsers, iaux-item-metadata, result-type and typescript-memoize —
whether or not they used the navigator at all.

It used one field from all of that: item.metadata.identifier. And that
is the one field upstream deliberately leaves unwrapped, because an
identifier can only ever be a string; every other Metadata field returns
a parsed wrapper the navigator never touched.

So `item` becomes `identifier: string`. The property is renamed rather
than just retyped, so a consumer still passing an object gets an unknown
property and fails visibly, instead of silently supplying a shape the
navigator can no longer read. It also matches the child element, which
already declared `identifier` — the boundary no longer translates.

The base64 attribute converter goes with it. That existed only because
the property held an object; a string attribute needs no decoding, and
neither consumer used the attribute form.

Worth knowing for the consumer migrations: the two need different fixes.
bookreader holds a MetadataResponse and wants .metadata.identifier;
offshoot passes public_metadata, a Metadata, and wants .identifier
directly — which is a live bug today, since .metadata is absent there
and the placeholder never receives an identifier at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The navigator required the host to inject a shared resize observer, then
used it for two things. Neither needed JavaScript.

The overlay/shift breakpoint is a container query now. It keys off the
frame's own width exactly as before — a narrow navigator in a wide page
still overlays — but the browser evaluates it during layout instead of
in a callback, so there is nothing to inject, register or tear down.
The `overlay`/`shift` classes and the `openMenuState` they came from
are gone; shift is the default and the query overrides it.

The header slot's registration went further than redundant. It existed
to re-render after measuring the slotted header's offsetHeight and
writing it back as an inline height. offsetHeight excludes margins, so
a header with vertical margins was pinned shorter than its own box and
its bottom margin overlapped the reader — clipped rather than visibly
broken, since the frame hides overflow. Dropping the measurement lets
the slot size itself, margins included, which fixes that.

Removing the registrations also removes a leak they carried: the slot
handler was an anonymous object created fresh on each call, so nothing
could remove it. It survived disconnect and observer swaps, holding a
detached element against the host's long-lived singleton.

`sharedObserver` and service-interfaces.ts go with it, so hosts no
longer have to supply anything. Consumers still setting the property
are unaffected — an unknown property on a Lit element is a no-op.

The tests changed shape to match. The old ones called handleResize and
asserted a state string, which would have passed even if the CSS never
worked; the replacements measure real layout, including two navigators
of different widths in one document — the case a media query cannot
express — and pin the header margin regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@iisa iisa changed the title Webdev 8458 migrate item navigator WEBDEV-8458 Migrate item-navigator into elements Aug 27, 2026
@iisa
iisa force-pushed the WEBDEV-8458-migrate-item-navigator branch from 13fdd80 to 30de5d1 Compare August 27, 2026 23:52
@iisa
iisa requested a review from jbuckner August 28, 2026 01:42
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.

3 participants