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

@xcwds/plugin-shell

v0.5.0

Published

The app shell for @xcwds apps: header with the page title and back arrow, a tab bar on phones, header links or a sidebar on wider screens, toasts and a Home page plugins add to.

Readme

@xcwds/plugin-shell

The app shell for an @xcwds app on @xcwds/sveltekit, from xcwds.github.io's root layout: a header with the page title (the page's only <h1>) and a back arrow, a tab bar on phones, header links or a sidebar on tablets and computers, one stack for toasts and notices, a Home page other plugins add to, and an error page.

// xcwds.config.ts
import { defineConfig } from '@xcwds/core';
import shell from '@xcwds/plugin-shell';

export default defineConfig({
	brand: { name: 'My app', tagline: 'Everyday tools.' },
	plugins: [
		shell({
			sections: [
				{ path: '/', label: 'Home', emoji: '🏠' },
				{ path: '/recipes', label: 'Recipes', emoji: '📖', also: ['/guide'] },
				{ path: '/settings', label: 'Settings', emoji: '⚙️' }
			]
		})
	]
});
<!-- src/routes/+layout.svelte -->
<script lang="ts">
	import Shell from '@xcwds/plugin-shell/Shell.svelte';
	import UpdateBanner from '@xcwds/plugin-update/UpdateBanner.svelte';
	import { App } from '@xcwds/sveltekit';
	import '../app.css';

	let { children } = $props();
</script>

<App>
	<Shell>
		{@render children()}
		{#snippet notices()}<UpdateBanner />{/snippet}
	</Shell>
</App>
<!-- src/routes/+page.svelte -->
<script lang="ts">
	import Home from '@xcwds/plugin-shell/Home.svelte';
</script>

<Home />

<!-- src/routes/+error.svelte -->
<script lang="ts">
	import ErrorPage from '@xcwds/plugin-shell/ErrorPage.svelte';
</script>

<ErrorPage />

Other pages render their content in <main class="page-narrow"> (tools, settings), <main class="page-wide"> (lists, Home) or <main class="page-split @container"> (narrow until xl, then wide enough for two columns, which the page lays out with @[50rem]: container queries), and their route's width (app.route(), default wide) names the same container so the header lines up with it. Pages don't render their own <h1> or back links.

Styles (Tailwind v4)

The components use Tailwind classes and CSS variables; Skeleton isn't needed. Tailwind doesn't scan node_modules, so import the shell's CSS, which adds its @source:

/* src/app.css */
@import 'tailwindcss';
@import '@xcwds/plugin-theme/tailwind.css'; /* optional: dark mode follows the theme setting */
@import '@xcwds/plugin-shell/styles.css';

It provides the sidebar: variant (the sidebar layout; use md: for anything tied to the tab bar), the page-narrow, page-wide and page-split utilities, and the accessibility baseline: 44px controls (checkboxes and radio buttons get theirs from their <label>), a :focus-visible ring and reduced motion. Theme it by setting the --xcwds-shell-* and --xcwds-toast-* variables on :root (and :root[data-color-scheme='dark']); see styles.css for the list.

Navigation

Phones (narrower than md, or under 500px tall) get the tab bar. Wider screens get header links or a sidebar per orientation, from the nav setting ({ portrait, landscape }, each 'bar' or 'sidebar'; defaults: bar in portrait, sidebar in landscape). It applies before first paint as data-nav-portrait / data-nav-landscape on <html>. <NavPicker> lets the user choose. With one section there is no navigation at all.

The header's title, emoji and back arrow come from the route registry; a section's own page uses its label, Home uses brand.name, and other pages lead back home. A section's also paths (and everything under them) highlight it too.

A section can belong to an app from @xcwds/plugin-tools' apps (app: 'kitchen'): it's hidden while that app is off, in the tab bar, header links and sidebar, and comes back when it's on, in this tab or another. plugin-tools' pre-paint script lists the apps that are off as data-apps-off on <html>, and the shell's CSS hides their sections, so a hidden tab never flashes. Pages under a hidden section still open. The build fails when an app isn't one of plugin-tools' apps, or when every section has one: one must always show.

For plugins

  • app.toast(message, { action?, durationMs? }) shows a short confirmation in the stack (3 s, or 8 s with an action link { label, path, hash? }; at most three at once). A save that fails (app.storage.onSaveFailure) shows one "Couldn't save on this device" toast. A toast records the page it was raised on (path); while @xcwds/plugin-lock has the app locked, one from a page it keeps out says "Private page" instead (the toaster's privateText).
  • app.shell.home.add(Component, { props?, order? }) adds a block to Home (lowest order first), below brand.name and brand.tagline.
  • app.shell.header.add(Component, { props?, order? }) adds a button beside the title.
  • app.shell.setTitle(text) titles the page showing now, for a page whose title isn't known at build time (an app made of blocks at /my-apps/app#<id>). The header shows it, and so does <title>, except on a private route, whose <title> keeps the route's title so the name stays out of the browser's history and app switcher. It holds for that path only (a fragment change keeps it; the next page shows its own title), is trimmed and cut at 120 characters, and setTitle(undefined) puts the route's title back. app.shell.title (get(), subscribe()) follows it.
  • app.shell.sections and app.shell.toasts (list(), subscribe(), dismiss(id)).

Register a client plugin that uses these after the shell in the config's plugins, so app.shell exists when it loads.