npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@campminder/components

v0.3.2

Published

> Published on public npm. Install and render: [`docs/consuming-the-shell.md`](../../docs/consuming-the-shell.md). > Working in here: [`CONTRIBUTING.md`](../../CONTRIBUTING.md).

Readme

@campminder/components

Published on public npm. Install and render: docs/consuming-the-shell.md. Working in here: CONTRIBUTING.md.

The Campminder chrome: left nav, top bar, super-user banner. Lit web components, composition-agnostic, rendering only what they are handed.

Why one package and not three

The lineage verdict on #25854 is explicit that side-nav and top-bar are "one shell, split across two packages by history, not by design." Porting them into three separate packages here would have carried that accident forward permanently — three version numbers, three release decisions, and a consumer installing three things to get one shell. Merging published packages later is a breaking change for everyone; merging them now, before anything publishes, costs nothing.

Subpath entries keep the granularity that actually mattered:

import '@campminder/components/side-nav'
import '@campminder/components/top-bar'

A consumer that only wants the left nav does not pull the top bar's code.

Dependencies are added by the floor that imports them

This package declares lit and nothing else. axios, @carbon/web-components and @material/web were carried over from the core-js manifests during the scaffold and removed 2026-09-02: nothing in this package imported them — and nothing in core-js's apps/side-nav, apps/top-bar or apps/super-user-banner imports them either, so they were dead weight upstream before they were dead weight here. They sat in dependencies, not devDependencies, so every consumer would have installed two whole design systems to render a menu.

If the Floor 7 top-bar port or the Floor 8 banner port needs one, the floor that writes the import adds it back. Do not pre-declare a dependency for a port that has not happened — an unused dependency in a published package is a cost paid by every consumer for nothing.

Port sources

| Entry | Port from | ADO floor | | ------------------- | ------------------------------------------------------ | ---------- | | side-nav | core-js/apps/side-nav (@campminder/side-nav 1.5.0) | Floors 3–4 | | top-bar | core-js/apps/top-bar (@campminder/top-bar 0.0.111) | Floor 7 | | super-user-banner | core-js/apps/super-user-banner (0.8.6) | Floor 8 |

core-js keeps building its copies for its existing consumers. This is a copy, not a move — Rule 2 of the verdict.

Strip on the way in

The port is the cheapest moment: there are no consumers here yet, so nothing needs to stay backwards-compatible. This is choosing what not to copy, not refactoring live code.

side-nav

  • BodyPaddingController — injects html body { padding-left: 168px !important; } into document.head. The shell's grid owns that space now.
  • initSearchIntegration() and its setTimeout(setupSearchBarListener, 100) re-arm — polls the DOM forever in any composition without search.
  • document.querySelector('search-bar') at SideNav.ts:129 and :477.
  • SIDENAV_STORAGE_KEY = 'isSideNavCollapsed' — a global key two shells on one origin would share. The shell namespaces it per portal.
  • src/data/data.ts — the base64 fixture. The tree is a property.

top-bar — ported Floor 7, 2026-09-08. This list is now a record, not a forecast.

  • The entire CDN script loader. loadWebComponents(), getCdnUrl(), WEB_COMPONENTS_PATH, BASE_DOMAIN, FALLBACK_CDN_HOST, the searchReady state and the webComponentsUrl property. It appended a <script type="module"> to document.head pointed at a hardcoded cdn.*.campminder.com host, purely so that search-bar would register itself. The plan did not know about this — it named the registry probe and the render, not the script injection, which is the more serious breach of the one rule. Making search a slot removed the whole apparatus.

  • customElements.get('search-bar') at TopBar.ts:58, the <search-bar> render at :333, and the enableSearchWidget property. Search is a named <slot name="search">, rendered unconditionally; a shell that wants it fills it, and nothing polls for it. search-bar is therefore not ported in Phase 1.

  • All search-expansion state — searchExpanded, handleSearchExpanded, handleSearchClose, renderSearchCloseButton(), and two document.dispatchEvent(new CustomEvent('search-expanded')) calls that existed so side-nav could hear them. Phase 1 ships no search, so the state could never become true. Owed when search is slotted in: upstream collapsed the page-info block and the user dropdown while search was expanded on mobile. Reintroduce that as something the slotted element drives, not as a document-level broadcast.

  • All positioning. topOffsetPx and leftOffsetPx, the isSideNavCollapsed state, the calculatedLeftOffset getter, syncWithSideNavState(), initSideNavIntegration(), handleSideNavToggle, and the import of shared/navigation-constants.ts (SIDENAV_ANIMATION, SIDENAV_DIMENSIONS, SIDENAV_STORAGE_KEY, SIDENAV_TOGGLE_EVENT). LAYOUT in @campminder/campminder-shell is the single source and the shell's grid applies it. The rendered <section> carries no style attribute at all. Reading side-nav's localStorage key was querying for a sibling.

  • All JS device detection. Utils/device-css-classes.ts and Utils/mobile-detection.ts are not here. Responsiveness is the media queries that were already in styles.ts alongside the class-driven duplicates. See TopBar.styles.ts's comment on the page-title breakpoint for the one behaviour-neutral simplification this exposed.

  • data/data.ts — the base64 fixture, and index.css — page-level :root and body styling, which is a host concern.

  • assets/Icons/CloseIcon.ts — copied with the other five, then removed: its only consumer was the search close button. side-nav carries its own CloseIcon, so nothing lost it. The remaining five live in assets/TopBarIcons/ behind a barrel, named XSvgIcon, matching side-nav's assets/SideNavIcons/ rather than upstream's unprefixed assets/Icons/ with one lowercase export.

  • Carbon and Material: nothing to strip. Verified 2026-09-08 that no file under core-js/apps/top-bar/src imports @carbon/web-components or @material/web. The standing note that retiring them is separate-and-optional still holds for the estate, but this port added neither, so this package still declares lit and nothing else.

  • @import url(fonts.googleapis.com/...) — missed by this floor and stripped on Floor 8, matching what side-nav had already done on Floor 4. See the banner section below for the measurement.

  • Seven upstream security patterns, refused rather than copied — link.url in href and in window.open(); popup_properties passed to window.open() verbatim, and then its UI-suppressing features; target="_blank" with no rel, and the popup path with no noopener; feature.value through lit's unsafeHTML; and icon_url / profile_url / logo_url reaching <img src> unguarded. Why each one is refused, and the trust boundary that makes them live rather than theoretical, is stated once — in Utils/safe-url.ts, with 46 unit tests beside it. Floor 8 was expected to inherit the question and did not — see below.

Old → new names, since this element is consumed by tag name:

| Upstream | Here | | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | <top-bar> | <cm-top-bar> | | data (base64 attribute) | unchanged — still data, still base64-decoded | | user | gone — its only consumer was <search-bar>'s config, which the host now supplies to whatever it slots | | top-offset-px, left-offset-px, enable-search-widget, web-components-url | gone | | — | <slot name="search"> |

super-user-banner

Renaming the element and its attributes is allowed. It is emitted as raw markup from ReactAppController.cs:163, so WebForms consumes it with no package edge — but that constrains the Phase 2 cutover, not this port: per Rule 2 this is a copy-forward and core-js keeps building the copy that page actually loads. Ruled 2026-09-02, reversing the ban that used to sit here.

  • position: fixed; top: 0; left: 0; width: 100%, and the ResizeObserver that compared scrollWidth against window.innerWidth to toggle an overflowed class (plus the shadow-root id that existed only for its getElementById). Both went because the shell owns layout. Why each one is refused, and the latent defect the observer was carrying, is stated once — in SuperUserBanner.styles.ts, which is where the replacement lives. Worth knowing at this level: the fixed positioning is why ReactAppController has to emit a hasSuperUserBanner class and prepend container markup at all, so deleting four declarations here deletes the reason for two host-side workarounds in Phase 2.
  • @import url(fonts.googleapis.com/…) and index.css (page-level :root and body styling, a host concern). The @import was also still in TopBar.styles.ts — side-nav dropped it on Floor 4, Floor 7 missed it, Floor 8 caught it. That file carries the reasoning and the measurement.

No URL guard was needed, and that is a finding. top-bar's Utils/safe-url.ts was left under top-bar/ on the argument that Floor 8 would be the second caller and would show the right shape. It is not: all three of the banner's values are database-sourced and cross the same untrusted boundary, but none is a URL and none is markup, so lit escaping text is the entire defence. A test asserts the component renders no a, img, iframe, source or script at all. Promoting the guards out of top-bar/ therefore still has exactly one caller and still is not justified.

It did need two fixes of a different kind, both found in review. A banner whose job is to make impersonation and environment unmistakable can fail at that job without any injection being involved:

  • Upstream's badge failed open — anything that was not exactly staging got the calm local badge, including the literal production and undefined that the host's GetEnvironmentName() really returns. A super-user impersonating in production saw the same badge as a laptop. There are now three badges and the unrecognised case is loud; environmentBadgeClass in SuperUserBanner.ts carries the reasoning.
  • The port had introduced a scroll container (overflow-x: auto) and hidden its scrollbar, with the badge as the last flex child — so a merely long client name pushed the badge out of view with no affordance, on a section with no tabindex. Upstream had no overflow rule at all, so this one was a regression, not an inheritance. The names now truncate and the scrollbar is no longer suppressed.

Old → new names, since this element is consumed by tag name:

| Upstream | Here | | ---------------------------------------------------- | ------------------------------------------------ | | <super-user-banner> | <cm-super-user-banner> | | superUser= (parsed as superuser) | super-user | | activeEnvironment= (parsed as activeenvironment) | active-environment | | client= | unchanged — client | | id="super-user-banner" in the shadow root | gone — existed only for the ResizeObserver |

The old spellings are ignored, not aliased, and a test asserts that — so the cutover fails loudly rather than silently rendering an empty banner.

The rule

Components render themselves; the shell owns layout. No component may touch document.body, document.head, or query for a sibling.

Dismissing the side-nav flyout

<cm-side-nav> exposes exactly one dismissal entry point:

dismissFlyout(restoreFocus: boolean): void

It closes the flyout when one is open and is a no-op when none is. Every trigger routes through it — the flyout's own Escape and shift-Tab paths, the navigation's mouseleave, the cm-mega-menu-close event (which now carries detail.restoreFocus), and the host shell's page-level Escape and outside pointer press — so the focus rule below is applied in one place and no trigger can bypass it.

The focus rule. Focus moves only when restoreFocus is true and focus is currently inside the flyout; then it goes to the category control that opened the flyout, falling back to the navigation's collapse control when that category control no longer exists. Focus is never left on the document body, and is never pulled into the navigation from somewhere else on the page — a person typing in a form field, or pressing a pointer outside the flyout, keeps the focus they have.

Two consequences worth keeping in mind when changing this code:

  • The focus target is read before the open-category state is cleared. The target is found via the category-hovered class, which is derived from that state; clearing first leaves nothing to find and silently reintroduces the dropped-focus bug.
  • Page-level Escape and outside-pointer listening belongs to the shell, which calls dismissFlyout on each slotted navigation — true for Escape, false for an outside pointer press. This component registers nothing on document or window, per the rule above, and mega-menu-isolation.test.ts observes that.