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

@net-advantage/jo-ui-shell

v2.1.0

Published

Application shell for JourneyOf. Toolbar nodes, groups, and destinations come from one version-1 navigation document. The document can be defined locally or returned by a server.

Readme

@net-advantage/jo-ui-shell

Application shell for JourneyOf. Toolbar nodes, groups, and destinations come from one version-1 navigation document. The document can be defined locally or returned by a server.

The words for this package are defined in LANGUAGE.md. Use those spoken names in issues and discussion. When a feature adds, renames, or retires a shell concept, update that glossary in the same change.

The shell follows a strict MVVM split:

  • Model functions are pure. They parse, normalize, index, and resolve the document.
  • The view-model hook owns selection, fallback, icon resolution, and the active destination page.
  • The view renders the view-model. It does not decide which item is active.

MVVM

flowchart LR
  Document["ShellNavigationDocument<br/>local object or server JSON"] --> Model["shell.model.ts<br/>parse, normalize, index"]
  Props["ShellProps<br/>controlled ids and callbacks"] --> Factory["createShellViewModel"]
  Model --> Factory
  Factory --> Observable["Observable ShellViewModel"]
  Observable --> Hook["useShellViewModel"]
  Hook --> View["Shell.tsx<br/>toolbar, groups, items"]
  View -->|"onToolbarSelect / onNavSelect"| Factory
  Factory -->|"onActiveNavItemChange"| Host["Host application"]

| Layer | File | Responsibility | | --- | --- | --- | | Contract | shell.types.ts | Document, slice, selection, page, and view-model types | | Model | shell.model.ts | Parse JSON, enforce hierarchy, index ids, build breadcrumbs, project toolbar/nav/header data, normalize navigation panel visibility, and map content padding onto shell root custom properties | | View-model | shell.view-model.ts | Framework-free createShellViewModel. Selection, fallbacks, callbacks, chrome text, and page status live on an Observable | | Hook | shell.use-view-model.ts | useShellViewModel subscribes to that observable and owns ids, refs, effects, focus trap, and the page lifecycle, including dispose on unmount, shallow page params, and the duplicate destinationId guard | | View | Shell.tsx | Render the toolbar, the logo, the navigation panel when it is visible, header, content, and footer. It resolves iconKey and host icons into nodes. Below 1024px it renders the mobile top bar, drawer, and bottom bar. | | Page | shell-page.view-model.ts | ShellPageViewModel chrome and status. updateParams stores params on the view-model and does not mutate the readonly context. samePageParams shallow-compares page params. activate?(): void \| Promise<void> is optional. defineShellPage in shell-page.tsx pairs the class with view slots | | View-model | shell-missing-page.view-model.ts | Missing-page trail and return-home action | | View | ShellMissingPage.tsx | Render the missing-destination page |

Models

flowchart TD
  Props["ShellProps"] --> Build["buildShellModel"]
  Document["ShellNavigationDocument"] --> Build
  Build --> ShellModel["ShellModel<br/>index, slices, presentation, actions"]
  ShellModel --> Toolbar["ShellToolbarItemViewModel[]"]
  ShellModel --> Groups["ShellNavGroupViewModel[]"]
  Props --> Header["ShellHeaderActionViewModel[]"]
  Props --> Padding["contentPaddingStyle<br/>block and inline lengths"]
  Toolbar --> ViewModel["ShellViewModel"]
  Groups --> ViewModel
  Header --> ViewModel
  Padding --> ViewModel
  MissingProps["ShellMissingPageProps"] --> MissingVm["ShellMissingPageViewModel"]

The view resolves icons in this order: an explicit icon, then a presentation icon from slices/toolbarItems, then resolveIcon(iconKey), then the default section icon. The view-model carries iconKey, not a rendered node.

Navigation models

flowchart TD
  Document["ShellNavigationDocument<br/>version: 1"] --> Toolbar["ShellNavigationNode<br/>kind: toolbar"]
  Toolbar --> Group["ShellNavigationNode<br/>kind: group"]
  Group --> Item["ShellNavigationNode<br/>kind: item"]
  Toolbar --> Slice["ShellSlice"]
  Group --> NavGroup["ShellNavGroup"]
  Item --> NavItem["ShellNavItem"]

Rules (this version supports exactly three levels: toolbar > navigation group > destination):

  • A toolbar node is a root. Its children must be groups.
  • A group is a heading in the navigation panel, not a link. Its children must be items. Groups do not nest.
  • An item is a destination and a leaf. It cannot contain children.
  • indexNavigation throws when a node breaks these rules, naming the parent and the offending child.
  • Every id is unique and non-empty.
  • Every node has an integer orderBy. Siblings are sorted by orderBy, then by id. Array position is not the order.
  • placement is only meaningful on toolbar nodes: top, primary, or bottom.

placement only chooses where a toolbar node sits. A bottom node with children is a section and navigates like any other toolbar item. A toolbar node with no children is an action: it reports its id and does not change the destination. Legacy slices, toolbarItems, and bottomToolbarItems can supply icons and order, but they do not remove sections defined by the document.

Server payload

{
  "version": 1,
  "nodes": [
    {
      "id": "settings",
      "kind": "toolbar",
      "label": "Settings",
      "orderBy": 10,
      "placement": "primary",
      "iconKey": "settings",
      "children": [
        {
          "id": "people",
          "kind": "group",
          "label": "People",
          "orderBy": 10,
          "children": [
            { "id": "users", "kind": "item", "label": "Users", "orderBy": 10 }
          ]
        },
        {
          "id": "access",
          "kind": "group",
          "label": "Access",
          "orderBy": 20,
          "children": [
            { "id": "roles", "kind": "item", "label": "Roles", "orderBy": 10 }
          ]
        }
      ]
    }
  ]
}
const navigation = parseShellNavigationDocument(payload);

<Shell
  navigation={navigation}
  resolveIcon={(iconKey) => icons[iconKey ?? '']}
  onActiveNavItemChange={setNavId}
/>

JSON cannot carry React nodes. Use iconKey in the document and resolveIcon in the host. Existing React icons passed through slices or toolbar overrides are preserved by the view.

Selection

  • Omit activeNavItemId for uncontrolled selection. Pass it to control selection from the host.
  • The active section is derived from the active destination. The host does not track a section id.
  • The shell does not own the browser address. The host reads the destination from the path and writes it when selection changes. browserShellAddress(target, slices) writes with history.pushState, so Back and Forward restore the destination.
  • A selected destination is written as /{sectionId}/{destinationId}. Home destinations are written as /{destinationId}. The short paths are read aliases: / selects Home's first destination, and /{sectionId} selects that section's first destination. An unknown path reads as no destination. The host can then pass missingPath.
  • Changing a toolbar section selects that section's first enabled destination.
  • Home is a toolbar node in the navigation document. It is selected the same way as any other section.
  • Disabled toolbar sections and destinations do not change selection.
  • viewContext.breadcrumbs is toolbar node > navigation group > destination. The last crumb is current. Header breadcrumbs items accept href or onClick and render as links or buttons. The current item is text, not a link.

Layout

  • Left toolbar: primary sections, bottom sections, and the user control. Bottom sections use placement: "bottom" and navigate the same way as primary sections.
  • Logo: the wordmark in the header row, above the navigation panel. Omit logo and the shell renders @net-advantage/jo-ui-logo. Pass logo to replace that wordmark. Inter and Caveat come from @net-advantage/jo-ui-theme/style.css. The logo stays visible when the navigation panel is hidden.
  • Navigation panel: the navigation groups of the active section, each a heading over its destinations. It sits under the logo.
  • navPanelVisible defaults to true, which keeps today's layout. Set it to false to omit the navigation panel only. The panel is not rendered and does not keep its column: the content area expands into the space under the logo, beside the toolbar. The logo and header stay on the top row.
  • Main area: shell header and the host outlet.
  • Main content padding uses --jo-ui-shell-content-padding-block and --jo-ui-shell-content-padding-inline. See Content padding.
  • The host passes one page from defineShellPage, plus optional pageParams. Define that page at module scope. The shell keeps one view-model per destinationId. It constructs that view-model when the active destination matches, calls activate once, calls updateParams when pageParams change by a shallow compare, and calls dispose when navigation leaves that destination or the shell unmounts. activate must catch its own errors and call setStatus({ kind: 'error', message }). The shell does not catch them. A new params object with the same keys and values does not call updateParams. A shell-context change does not construct another view-model. A page whose destinationId does not match the active destination is not mounted. Outside production, and outside Vite hot reload, a different createViewModel for a destinationId that already has a live view-model throws. A new definition object with the same createViewModel does not. Production replaces that view-model when the factory differs.
  • When the page is mounted and ready, view.content is the outlet. children is used only when no page matches. Omit both and the shell renders ShellMissingPage. Pass missingPath to name the address that did not match.
  • When the page is mounted, its chrome title wins over the shell title prop, and its header-action data wins over the shell headerActions prop. A view slot (headerActions, subtitle, footer, loading, error) wins over the matching chrome data. Shell title, breadcrumbs, and headerActions apply when no page is mounted. Disabled header actions do not report.
  • Page header actions carry iconKey. Shell headerActions may still pass a React icon. The header action row keeps the class jo-ui-shell__header-actions, including when a view slot supplies the actions. That class is a public test hook.
  • Header actions use variant: icon (the default), primary, secondary, or ghost. primary, secondary, and ghost show the label. badge is an optional mark. Set pressed to render aria-pressed and the token-based on state. title is an optional tooltip. The desktop and mobile buttons describe it with aria-describedby and show it on hover and keyboard focus. A blank title is ignored.
  • Status loading renders <Shadow variant="form" /> unless view.loading is set. The label defaults to Loading. Status error renders the message and a jo-ui Button (variant="primary") labelled Retry when retry is set, unless view.error is set.
  • Below 720px the header row grows and labelled actions wrap onto the next line instead of overflowing.
  • Footer spans the navigation and content columns. Hiding the navigation panel does not change that span; the content grows to meet it. A mounted page view.footer slot wins. Omit it and the shell renders the footer prop.
  • theme="dark" switches the shell palette.

Content padding

The main content region, .jo-ui-shell__main-content, reads two custom properties. Each property falls back to today's padding when it is not set, so the shell looks the same until a page opts in.

| Token | Above 1100px | 1100px and below | | --- | --- | --- | | --jo-ui-shell-content-padding-block | 1.25rem | 1rem | | --jo-ui-shell-content-padding-inline | 1.75rem | 1.25rem |

The mobile shell (1023px and below) has no separate content padding. It uses the 1100px fallbacks: 1rem block and 1.25rem inline.

Set either variable on the shell root or on an ancestor. A defined variable replaces the fallback at every width. Set different values per breakpoint with your own media queries on that ancestor.

contentPadding writes the same variables as inline style on the shell root. A blank or omitted length leaves that axis on the fallback. When both contentPadding and rootProps.style set a variable, contentPadding wins for the axes it sets.

<Shell contentPadding={{ block: '12px' }} />
<Shell
  rootProps={{
    style: {
      ['--jo-ui-shell-content-padding-block']: '12px',
    },
  }}
/>

Journey Details uses 12px block padding so the content gap matches the Sarah mock. Inline padding stays on the breakpoint default unless the page sets it.

Structure

  • src: view, model, view-model, types, and styles
  • stories: Storybook docs and the navigation document example
  • LANGUAGE.md: ubiquitous language for shell regions, nodes, selection, and MVVM

Publish footprint

This package publishes only:

  • dist
  • README.md
  • LANGUAGE.md