WEBDEV-8458 Migrate item-navigator into elements - #64
Conversation
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>
|
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
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>
| 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> |
There was a problem hiding this comment.
Can we move these into standalone svg files so we're not packaging them as part of the JS bundle?
There was a problem hiding this comment.
moved them to standalone .svg files — rendered with mask-image so they stay recolorable.
| /** Handler invoked by a shared resize observer when its target resizes. */ | ||
| export interface SharedResizeObserverResizeHandlerInterface { | ||
| handleResize(entry: ResizeObserverEntry): void; | ||
| } |
There was a problem hiding this comment.
Can we import this from @internetarchive/shared-resize-observer instead of re-creating it?
|
Can we scope the name of all of the elements, ie |
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>
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>
|
@jbuckner thanks for the review — pushed a batch addressing most of it. Replies are on the individual threads; summary here. AddressedExternalized the SVGs ( Scoped the element names ( Each element now declares its tag in Dropped the modal manager ( Removed the redundant Still open
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.
|
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>
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
…em-nav-open-close-semantics
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>
13fdd80 to
30de5d1
Compare
WEBDEV-8458 Migrate item-navigator into elements
Ports
@internetarchive/iaux-item-navigatorinto elements asia-item-navigator, re-styled to the elements CSS conventions, with a live demo and full test coverage.main: https://internetarchive.github.io/elements/What ships
A fullscreen-capable shell —
header/mainslots, a drawer driven by amenuContentsprovider array, a minimizedmenuShortcutsrail, overlay/shift at 600px — plus its internals (ia-itemnav-menu-slider,-menu-button,-no-theater-available) and two bundled menus:-viewable-files-panelwith-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 fromsrc/elements/index.ts.Key decisions
ia-itemnav-— tags, files and classes. Not cosmetic: upstream registersia-menu-slider,ia-itemnav-loaderandia-no-theater-availableverbatim, and elements re-exports the navigator from its root barrel — so any@internetarchive/elementsimport would register those twice and throwNotSupportedErrorat module evaluation while consumers still load the old package..svgfiles — ~18KB of path data out of the JS bundle — drawn through amaskedIcon()helper so they stay recolorable.loadedstill gates the reader; an indicator returns on WEBDEV-8951.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-serviceis gone. It sat independencies, so every consumer of this package installed it — plusfield-parsers,iaux-item-metadata,result-typeandtypescript-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. Soitemis nowidentifier: 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:
inert, so tab order and the accessibility tree match the screen, andaria-expandedreports what's actually open.firstUpdatedfired 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/openMenuwhile the slider held its ownselectedMenu, plusopenandanimateMenuOpen. SincemenuTypeSelectedis 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 setslider.openonce, so it stayedtrueforever; nothing ever setanimateMenuOpen, so.animatenever 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.
openMenuStateand theoverlay/shiftclasses 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'soffsetHeightand write it back as an inline height.offsetHeightexcludes 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.
sharedObserverandservice-interfaces.tsgo 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-:base-font-size10px·menu-width320px·menu-margin42px·animation-timing200ms·icon-size2.4em·header-icon-size2em·menu-button-label-displaynonetext-colorvar(--true-white)·icon-colorandicon-active-color→ text color ·icon-inactive-colorvar(--lighter-gray)·border-color#4b4b4b·active-file-border-color#538bc5theater-bg-color#000·menu-slider-bg#212121·active-button-bgvar(--mid-gray)·share-embed-bg#151515Review responses
.svgfiles, ~18KB of path data out of the bundleia-itemnav-*across tags, files, classesrender()need thehtmlwrapper?CustomTheaterInterfaceoverreaches; how does it meet offshoot's<ia-theater>?ModalManager?SharedResizeObserver?service-interfaces.tsis deletedia-no-theater-availablemeet offshoot's?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
Menus
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.Automated —
pnpm test(110 navigator tests of 304),pnpm run buildand the madge circular check pass. The navigator covers 100% of lines and functions, 97% of branches.Follow-ups (tracked, out of scope)
Consumer note
Two breaking changes land here, both free while the component is unreleased, and both already on the migration tickets:
item→identifier, and the two consumers need different fixes. bookreader holds aMetadataResponseand wants.metadata.identifier; offshoot passespublic_metadata, aMetadata, and wants.identifierdirectly — which is a live bug today, since.metadatais absent there and the placeholder never receives an identifier.sharedObserveris no longer read. Consumers can drop the binding; leaving it in place is harmless.