Skip to content

Webdev 8458 migrate item navigator - #64

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

Webdev 8458 migrate item navigator#64
iisa wants to merge 17 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 the elements repo as a
production component (ia-item-navigator), re-styled to the elements CSS
conventions, with a live demo and full test coverage.

What's included

The full upstream package (16 source files) — the navigator shell and both bundled menus:

  • ia-item-navigator — fullscreen-capable frame: header/main slots, a collapsible drawer driven by a menuContents provider array, a minimized menuShortcuts rail, responsive overlay/shift, loader, and no-theater placeholder.
  • ia-menu-slider, ia-menu-button, ia-itemnav-loader, ia-no-theater-available — the shell's internal pieces.
  • ia-viewable-files-panel + ia-sort-files-button — viewable-files menu (scrollable volume list, active-file highlight, PDF flagging, tri-state title sort).
  • ia-share-panel — social share links + copyable iframe/bbcode embed snippets.

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

Key decisions

  • Naming: adopted the elements ia-* / IA* convention across all tags and classes (upstream mixed iaux-*, un-prefixed, and Lit-2 casing).
  • Zero runtime deps added, except one: modal-manager / shared-resize-observer were type-only upstream → replaced with local interfaces; all 11 icons inlined as Lit templates. Only new runtime dep is @internetarchive/metadata-service (genuinely used in the item converter).
  • Theming: public knobs exposed as --item-navigator-* with private --x-- aliases + inline defaults. Consolidated onto a small palette: --item-navigator-border-color, --item-navigator-text-color, --item-navigator-icon-color / -active-color / -inactive-color, --item-navigator-share-embed-bg, plus surface/animation knobs.
  • Sizing: anchored to a self-contained 10px base (--item-navigator-base-font-size) with em units, reproducing petabox's scale without forcing a 10px root on the rest of the demo.

Notable fixes vs. source

  • Menu toggle bound @click=${this.toggleMenu}, setting menuOpened to a PointerEvent (only "worked" because truthy) → now stays boolean.
  • Drawer sub-panel closes independently of the drawer; theater stays flush with the drawer edge through the menu animation (shift ≥600px / overlay <600px); fullscreen now fills the viewport; slider header action fixed.
  • execCommand('copy') → async Clipboard API (with fallback); sort works on a copy instead of mutating the reactive prop; dropped a dead never-rendered method.

Demo

Self-contained ia-item-navigator story (Production-Ready): realistic archive.org items (images / PDFs / video) where clicking a viewable file swaps the theater iframe; slotted header + theater; share panel + populated shortcut rail; and a Styles panel that lists every theming/layout toggle, shows the CSS var each control sets, and has a Reset.

Testing

  • 76 navigator tests (Vitest browser mode); all eligible runtime files at 100% coverage.
  • npm run build, npm run lint, and the madge circular check all pass.

Follow-up (out of scope, tracked)

Shared dark-surface palette tokens for the near-blacks / border / scrim that have no house equivalent today — WEBDEV-8796 / #66.

QA Steps

Setup: open the live demo → Production-Ready → <ia-item-navigator>, expand Import, Usage & Settings.

  • Renders — dark theater frame + header bar; left rail shows Viewable Files, Share, and ⋯, all ~24px.
  • Drawer — ⋯ animates open/closed; while closed, Tab doesn't enter it.
  • Shortcuts — clicking Viewable Files / Share opens the drawer straight to that panel.
  • Sub-panel close — header ✕ closes the panel but leaves the drawer open; Esc closes panel first, then drawer.
  • Viewable Files — long titles wrap, PDFs tagged, active file highlighted; sort cycles neutral→asc→desc; clicking a file swaps the theater iframe.
  • Share — social links open real URLs; embed block copies and flashes "Copied".
  • Toggles — Loaded (spinner↔theater), View available (placeholder), Header (hides bar), Fullscreen (fills viewport + in-frame Exit), Animate (instant↔animated).
  • Responsive — >600px: theater eases in sync with the drawer; <600px: drawer overlays a full-width theater.
  • Theming — each Styles control shows the CSS var it sets and updates live; Reset restores defaults.
  • A11y — menu buttons keyboard-operable; close buttons labelled; ::part(minimized-menu) targets the rail.
  • Automatednpm test (76 navigator tests), npm run build, npm run lint pass.

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-07-25 04:54 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 96.49682% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.42%. Comparing base (cd1ffd3) to head (7a790b9).

Files with missing lines Patch % Lines
demo/story-components/story-styles-settings.ts 0.00% 10 Missing ⚠️
demo/story-template.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #64      +/-   ##
==========================================
+ Coverage   78.62%   84.42%   +5.80%     
==========================================
  Files          17       27      +10     
  Lines         697     1008     +311     
  Branches      189      255      +66     
==========================================
+ Hits          548      851     +303     
- Misses        100      108       +8     
  Partials       49       49              

☔ 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?

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.

Done — all 13 glyphs are standalone .svg files now (~18KB of path data out of the JS bundle). Verified none of the path strings remain in the emitted dist JS.

ia-item-navigator/icons/ellipses.svg
ia-item-navigator/icons/collapse-sidebar.svg
ia-item-navigator/menus/icons/share.svg
ia-item-navigator/menus/icons/twitter.svg
ia-item-navigator/menus/icons/facebook.svg
ia-item-navigator/menus/icons/tumblr.svg
ia-item-navigator/menus/icons/pinterest.svg
ia-item-navigator/menus/icons/email.svg
ia-item-navigator/menus/icons/link.svg
ia-item-navigator/menus/icons/viewable-files.svg
ia-item-navigator/menus/icons/sort-asc.svg
ia-item-navigator/menus/icons/sort-desc.svg
ia-item-navigator/menus/icons/sort-neutral.svg

One wrinkle worth flagging, since it drove the approach: these icons are recolored via CSS (--item-navigator-icon-color, plus active/inactive states on the menu buttons). <img src> — the pattern ia-combo-box and ia-dropdown-search-bar use — can't be recolored, so it would have dropped that theming.

So they render through a small maskedIcon() helper using mask-image + background-color: the mask supplies the shape, CSS supplies the paint. Same .svg-import mechanism as the other components, still themeable. Host CSS moved from fill to background-color, and the original .fill-color rules are kept alongside it so any inline-<svg> icon a consumer passes in (bookreader's bookmarks/search/downloads providers) keeps theming with no change on their side.

The loader's book/ring is the one exception and stays inline — a mask can't animate the spinning sub-path.

Also worth knowing if you hit this elsewhere: the url() has to be quoted. Bundlers inline small SVGs as data: URIs whose markup contains apostrophes, and an unquoted url() silently drops the whole mask-image declaration — the glyph then paints as a solid block.

computer.
</p>
<a href=${this.downloadUrl}>Show all files</a>
</section>

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.

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.

we can remove. this is legacy from porting, currently live in petabox

/** 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!

* `HTMLElement` captures that contract without depending on
* `@internetarchive/modal-manager`.
*/
export type ModalManagerInterface = HTMLElement;

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.

What happens when we use the actual ModalManager?

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.

Nothing happens — which is why I've removed it rather than keeping the local type.

The navigator accepted modal and never read it. Nothing downstream read it back off the navigator either: in bookreader the menu providers get the modal manager directly through baseProviderConfig (ia-bookreader.js:346-357), a path that doesn't go through the navigator at all — that's how BookmarksProvider ends up calling this.modal.showModal(). Offshoot never passed one.

Worth noting it's vestigial upstream too: iaux-item-navigator has exactly two modal mentions, the import and @property() modal?: ModalManager — it never forwards or calls it. So the port wasn't dropping behavior; there wasn't any.

ModalManagerInterface is gone with it, so service-interfaces now describes only the resize observer, which the navigator does genuinely use.

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. Confirmed in the demo: assigning .modal after the removal doesn't throw and the navigator renders and operates normally.


/** Icon for the viewable-files menu shortcut / menu entry. */
export const viewableFilesIcon = html`
<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.

Same comment... can we externalize this so it doesn't inflate the bundle size?

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.

Done — this one is menus/icons/viewable-files.svg now, alongside the other twelve. Same maskedIcon() treatment as the rest (details in the icons.ts thread): mask supplies the shape, background-color supplies the paint, so it still follows --item-navigator-icon-color.

Worth noting for consumers: this module's viewableFilesIcon export keeps its name and its TemplateResult type — it just renders a masked <span> now instead of an inline <svg>. bookreader and offshoot both import it, and neither needs a change unless they target it with their own svg/path CSS.

* matching the current `subPrefix`. Populated by the host via `fileList`
* (typically re-ordered by an `ia-sort-files-button`).
*/
@customElement('ia-viewable-files-panel')

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.

Looking at a lot of these custom elements, I think we should somehow scope their names to the element they're a part of since they're going to exist in the global namespace, ie ia-itemnav-viewable-files-panel

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.

Done — sub-elements are now scoped under ia-itemnav-* (tags, files and classes). Files changed:

Renamed

ia-itemnav-loader.ts                  → ia-itemnav-loading-view.ts
ia-itemnav-loader.test.ts             → ia-itemnav-loading-view.test.ts
ia-menu-slider.ts                     → ia-itemnav-menu-slider.ts
ia-menu-slider.test.ts                → ia-itemnav-menu-slider.test.ts
ia-menu-button.ts                     → ia-itemnav-menu-button.ts
ia-menu-button.test.ts                → ia-itemnav-menu-button.test.ts
ia-no-theater-available.ts            → ia-itemnav-no-theater-available.ts
ia-no-theater-available.test.ts       → ia-itemnav-no-theater-available.test.ts
menus/ia-share-panel.ts               → menus/ia-itemnav-share-panel.ts
menus/ia-share-panel.test.ts          → menus/ia-itemnav-share-panel.test.ts
menus/ia-sort-files-button.ts         → menus/ia-itemnav-sort-files-button.ts
menus/ia-sort-files-button.test.ts    → menus/ia-itemnav-sort-files-button.test.ts
menus/ia-viewable-files-panel.ts      → menus/ia-itemnav-viewable-files-panel.ts
menus/ia-viewable-files-panel.test.ts → menus/ia-itemnav-viewable-files-panel.test.ts

Added

src/elements/ia-item-navigator/element-names.test.ts

Modified

README.md
src/elements/index.ts
src/elements/ia-item-navigator/ia-item-navigator.ts
src/elements/ia-item-navigator/ia-item-navigator.test.ts
src/elements/ia-item-navigator/ia-item-navigator-story.ts

ia-item-navigator itself keeps its name. The loader became ia-itemnav-loading-view rather than ia-itemnav-loader because @internetarchive/ia-item-navigator already registers that exact name — along with ia-menu-slider and ia-no-theater-available, so those three would have thrown on double registration while consumers still load both packages.

inline: 'nearest',
});
}
}, 350);

@jbuckner jbuckner Jul 27, 2026

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.

What is the intended behavior here? This feels code smelly to put an arbitrary 350ms timeout on this scroll. Can we be more deterministic about this?

}

render(): TemplateResult {
return html`${this.fileList.length ? this.fileListTemplate : nothing}`;

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.

Do you need to wrap this in html since you're just returning the template or nothing?

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.

No — good catch, it was only there to satisfy the TemplateResult return type. Widening the signature lets the branch return as-is:

render(): TemplateResult | typeof nothing {
  return this.fileList.length ? this.fileListTemplate : nothing;
}

Existing coverage already asserts the empty case renders no ul, and I confirmed in the demo that the list still renders populated (7 items), disappears when fileList is emptied, and comes back on repopulate.

* The contract a theater host implements to feed menu providers and shortcuts
* to the item navigator.
*/
export interface CustomTheaterInterface extends LitElement {

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.

Based on the comment regarding "providers and shortcuts", this interface seems to be doing a lot more than that... passing itemMD, baseHost, observer, modal, etc. Can we scope this more concretely to those behaviors?

Also, how does this interact with offshoot's <ia-theater>?

* A tiny adapter that satisfies the navigator's `SharedResizeObserverInterface`
* using a single native `ResizeObserver`, so the demo can show the responsive
* overlay/shift behavior without depending on
* `@internetarchive/shared-resize-observer`.

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.

Why not just import the shared resize observer?

@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?

ModalManagerInterface,
SharedResizeObserverInterface,
SharedResizeObserverResizeHandlerInterface,
} from './interfaces/service-interfaces';

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.

What happens when we use the actual Modal Manager and Shared Resize Observer?

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.

Two different answers, since the navigator treats them very differently.

Modal manager — nothing happens, so I've removed the property (details in the service-interfaces.ts thread). The navigator never read it, and nothing downstream reads it back: bookreader's providers get the modal directly via baseProviderConfig, bypassing the navigator entirely. It was unused upstream too.

Shared resize observer — a real instance drops straight in, because this one is genuinely load-bearing. The navigator implements the handler interface and uses it for real behavior:

  • observes #frame and flips between overlay and shift at the 600px breakpoint
  • observes the header slot and re-renders when its height changes
  • removes both on disconnect and when the observer property is swapped

The local interface is just the structural subset it calls — addObserver / removeObserver with { target, handler } — which a real SharedResizeObserver already satisfies. bookreader passes one today (new SharedResizeObserver() at ia-bookreader.js:129) and it works unchanged.

Happy to import the type from @internetarchive/shared-resize-observer instead of declaring it locally, per your other comment — that's a type-only import so it stays out of the runtime dependency tree either way.

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.

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