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

@medalsocial/meda

v3.15.0

Published

Shared Meda UI shell and runtime package.

Readme

@medalsocial/meda

Shared UI shell and runtime primitives — the navigation chrome, panels, auth controls, recipes, theme bridges, command palette, and workbench layout that power Medal's apps. Published as Apache-2.0.

npm

Install

pnpm add @medalsocial/meda lucide-react

Peer deps: react >= 19, react-dom >= 19, and lucide-react.

Feature peers are optional — install them only for the entry points that use them (package managers no longer auto-install them):

| Peer | Needed by | | --- | --- | | @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities | @medalsocial/meda (root barrel), /shell, /kanban, /email-builder | | @xyflow/react | @medalsocial/meda (root barrel), /workflow-builder. (Its stylesheet is vendored into styles.css / workflow-builder/styles.css, so the CSS compiles without the package.) | | three, @react-three/fiber | /voice (VoiceOrb) | | react-markdown, remark-gfm, rehype-highlight | /markdown-view | | next-themes | NextThemesAdapter / themeAdapter="next-themes" in /shell |

# Full Medal app (root barrel / shell):
pnpm add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities @xyflow/react

Lean surfaces such as @medalsocial/meda/calendar, @medalsocial/meda/booking and @medalsocial/meda/primitives need none of them. Fonts are not bundled either: the tokens name Geist / Geist Mono, so load them yourself (for example @fontsource-variable/geist + @fontsource-variable/geist-mono, or next/font).

Upgrading to 3.0

3.0 makes the feature peers optional, so npm and pnpm no longer install them for you. Nothing else in the API changed.

If you import any of the root @medalsocial/meda, @medalsocial/meda/shell, /email-builder, /kanban, /workflow-builder, or @medalsocial/meda/styles.css, add the peers to your own package.json:

pnpm add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities @xyflow/react

If you render the 3D VoiceOrb (@medalsocial/meda/voice), also add:

pnpm add three @react-three/fiber

three used to be a regular dependency of meda; it is now an optional peer. The @fontsource-variable/geist* packages are no longer installed by meda either — if you @import them, list them yourself.

Lean consumers need nothing: @medalsocial/meda/calendar, @medalsocial/meda/primitives, styles/bridge.css, styles/base.css and the per-feature stylesheets never reach an optional peer (see Lean consumer setup).

Booking screens (@medalsocial/meda/booking)

Unbranded, presentational screens for a customer booking flow: the wizard steps (WhoScreen, ServiceScreen, StylistScreen, TimeScreen, DetailsScreen, SummaryBar, Confirmation, BookingSkeleton, AddChildSheet), the desktop's sticky side panel (BookingPanel), ManageScreen, login (LoginSheet, LoginPanel, OtpSlots, VippsButton) and the customer portal (PortalShell, UpcomingBookings, VisitHistory, ChildCards, RebookCards, FamilyEditor, ProfileForm, DataControls, AccountCard, PortalUnreachable, …).

They are props in, callbacks out: nothing fetches, routes or touches storage. The data is plain structural shapes (BookingServiceDto, WizardState, …), every date and price goes through the format prop (BookingFormat: the business's clock, money, phone and name formatting), and every word through labels — meda ships no default language and no brand. Override, cheapest first:

  1. CSS variables — the shadcn tokens the bridge maps (--primary, --card, --muted, --border, --ring, --radius, …). Colours come from nowhere else.
  2. labels — a flat record ('who.heading', …); BookingLabels is the full set and BOOKING_LABEL_KEYS lists it. {placeholders} are filled by the screen. A value is a BookingLabel: a string renders as one text node once filled (like a template literal), a string array renders one text node per element, each element filled with the same values (see below).
  3. classNames — per documented slot (root, card, cardSelected, chip, …), merged after the defaults so an override wins.
  4. components — card renderers (ServiceCard, StylistCard, TimeChip, DayChip, PersonCard, …). The defaults are exported (DefaultServiceCard, …) so an override can wrap them.

Where a sentence breaks into text nodes

A browser lays text out per DOM text node, so the same sentence split differently can land its later glyphs a sub-pixel apart. The pack decides; a screen never splits a string on its own:

// One text node: «Total 840 kr · paid at the salon».
const asOneNode = {
  'confirmation.party.total': 'Total {total} · paid at the salon',
} satisfies Partial<BookingLabels>;

// Four text nodes, the DOM of `Total {total}{' · '}paid at the salon` in JSX.
const asPieces = {
  'confirmation.party.total': ['Total ', '{total}', ' · ', 'paid at the salon'],
} satisfies Partial<BookingLabels>;
  • An element that fills to '' renders nothing; a label that fills to nothing renders nothing at all.
  • Lines joined from several parts (the confirmation card's service · stylist · when · price, the portal's booking lines) are one text node while every part is a string, and in pieces (each part and separator its own node) once one of their labels is an array. joinLabelParts(parts, separator, { pieces }) forces either.
  • Text-only targets — aria-label, placeholder, file names, error and status messages, live-region announcements, a value filled into another label, and a custom component's string prop — read the label flattened (fillLabel, labelText).
  • Helpers, for UI composed around the screens: renderLabel(label, values) (element children), fillLabel(label, values) (text), labelParts(label, { hole: <el/> }) (element holes), fillLabelPieces, labelText, joinLabelParts.
@import 'tailwindcss';
@import '@medalsocial/meda/styles/bridge.css';      /* your own shadcn tokens */
@import '@medalsocial/meda/primitives/styles.css';
@import '@medalsocial/meda/booking/styles.css';

Tailwind CSS v4 setup

Meda ships a styles.css with its design tokens. Import it once in your entry stylesheet or entry script:

@import 'tailwindcss';
@import '@medalsocial/meda/styles.css';

:root {
  /* Consumer overrides go after meda so equal-specificity tokens win by source order. */
  --color-brand-500: oklch(0.62 0.18 245);
  --auth-gradient-primary: var(--color-brand-500);
}

.dark {
  --color-brand-500: oklch(0.72 0.16 245);
}

Avoid placing token overrides before the Meda import; tokens.css defines the package defaults and later declarations are what override them:

/* Wrong: meda's imported defaults overwrite this block. */
:root {
  --color-brand-500: oklch(0.62 0.18 245);
}

@import '@medalsocial/meda/styles.css';

Lean consumer setup

styles.css is batteries-included: it scans every meda component so Tailwind generates all of their utilities (~22 KB gzip of CSS), and it pulls in the @xyflow/react stylesheet. That is right for full Medal apps; it is overkill for a marketing or customer site that renders a calendar and a few primitives. Use the lean entries instead — each feature stylesheet generates only the utilities its own compiled output uses:

@import 'tailwindcss';

/* 1. Foundation — pick ONE */
@import '@medalsocial/meda/styles/base.css';   /* meda tokens + theme bridge + base layer */
/* @import '@medalsocial/meda/styles/bridge.css'; */ /* theme bridge only: bring your own tokens */

/* 2. Only the surfaces you render */
@import '@medalsocial/meda/primitives/styles.css';
@import '@medalsocial/meda/calendar/styles.css';

| Entry | Contains | | --- | --- | | styles.css | styles/base.css + every feature (full component scan + xyflow CSS). Unchanged for existing consumers. | | styles/base.css | tokens.css + bridge.css + the color-scheme base layer + Geist font mapping. No component scan, no xyflow. | | styles/bridge.css | @custom-variant dark + the @theme inline mappings (bg-primary, text-muted-foreground, bg-brand-500, ...). No token values, no fonts. | | styles/tokens.css | Raw CSS custom properties only. | | primitives/styles.css | Utilities for @medalsocial/meda/primitives (Button, Input, Textarea, Checkbox, Field, Sheet, ToggleGroup, Avatar, Card, Skeleton, StatusPill, EmptyState, FilterRail; not MarkdownView). | | calendar/styles.css | Utilities for @medalsocial/meda/calendar. | | booking/styles.css | Utilities for @medalsocial/meda/booking. Import primitives/styles.css too — the screens are built on the primitives. | | workflow-builder/styles.css | @xyflow/react's stylesheet + utilities for @medalsocial/meda/workflow-builder. The only lean entry that ships xyflow CSS. |

Feature stylesheets never include the foundation, so importing several of them does not duplicate tokens — import exactly one foundation first. Never combine the lean entries with styles.css (it already contains all of them).

Bring your own tokens. meda components use shadcn-style semantic tokens (--background, --foreground, --primary, --primary-foreground, --card, --muted, --muted-foreground, --accent, --border, --input, --ring, --destructive, ...). A site that already defines those names can import styles/bridge.css instead of styles/base.css, and meda parts re-theme to the site's palette automatically. bridge.css does not map --font-sans, so the site's own font stack is kept.

Usage

Use AppShell for the styled shell surface:

import { AppShell, MedaShellProvider } from '@medalsocial/meda/shell';
import { Inbox } from 'lucide-react';

export function App() {
  const workspace = { id: 'workspace', name: 'Workspace' };
  const apps = [{ id: 'inbox', label: 'Inbox', icon: Inbox }];

  return (
    <MedaShellProvider workspace={workspace} apps={apps}>
      <AppShell
        variant="workspace"
        iconRail={{
          mainItems: [{ id: 'inbox', label: 'Inbox', icon: Inbox, to: '/inbox' }],
        }}
      >
        {/* your app */}
      </AppShell>
    </MedaShellProvider>
  );
}

For framework routing, pass a render callback and forward Meda props:

import Link from 'next/link';

<AppShell
  variant="workspace"
  iconRail={{
    mainItems,
    renderLink: ({ item, linkProps }) => <Link {...linkProps} href={item.to} prefetch />,
  }}
  appTabs={{
    renderLink: ({ app, linkProps }) =>
      app.to ? <Link {...linkProps} href={app.to} prefetch /> : <a {...linkProps} />,
  }}
>
  {children}
</AppShell>;

Workspace shells also expose chrome-level composition slots:

<AppShell
  variant="workspace"
  headerCenter={<SectionTabs />}
  banners={<SystemHealthBanner />}
  mainLayout="fullbleed"
  mainClassName="marketing-main"
  workspace={{
    menuItems: [{ id: 'settings', label: 'Settings', href: '/settings' }],
    menuFooter: <AccountSwitcher />,
  }}
>
  {children}
</AppShell>

Rail header (headerLayout="rail")

By default the desktop header is a split grid: the workspace switcher and headerLeading share the left region, and headerCenter owns the middle. Because the switcher is as wide as the workspace name, the leading region both moves per workspace and never gets more than about half the window. headerLayout="rail" swaps in a three-column grid instead — rail column · fill · actions:

<AppShell
  variant="workspace"
  headerLayout="rail"
  iconRail={{ mainItems, labelVisibility: 'visible' }}
  headerLeading={<SectionTabs />}          // owns the whole fill column
  // Keep the panel views, drop the header's toggle:
  rightPanel={{ panelViews, showToggle: false }}
  globalActions={<NewButton />}
>
  {children}
</AppShell>
  • Column 1 is exactly the icon rail's width (--shell-rail-label-width when iconRail.labelVisibility is visible, --shell-rail-width otherwise) and holds the workspace switcher in its tile variant: the mark centred on the rail's axis with the workspace name beneath it, in the icon-rail label type. The dropdown is the chip's, unchanged.
  • Column 2 is headerLeading, min-w-0, and gets every remaining pixel.
  • Column 3 is globalActions plus the panel toggle.
  • headerCenter is ignored in this layout — there is no centre column.

The tile is metered to 44px — a 28px mark, a 2px gap and ONE 14px label line, with no vertical padding — and carries max-h-full inside an overflow-hidden column. So it fits whatever you set --shell-header-height to, down to 52px (the web app runs 52px under its desktop window-tab strip), and can never paint past the header:

/* a tighter header — the tile follows it */
:root { --shell-header-height: 52px; }

Because the label is truncated to one line — and hidden entirely below 700px viewport height, the same tier at which every icon-rail label hides — the full workspace name is always recoverable two other ways: the tile's tooltip, and its accessible name ("<workspace> workspace menu").

Both layouts put data-meda-shell-header on the <header> (with data-meda-header-layout="split" | "rail"), so consumer CSS and tests can target the header without depending on its structure.

Mobile nav: light by app

mobileNav.activeTo marks the exact address. Pass mobileNav.activeId (the active APP's id) as well and the dock lights whenever item.id === activeId, so it stays lit on every route inside that app rather than only on its first tab; the workspace sheet then opens with that row expanded and scrolled into view. mobileNav.currentFirst additionally hoists that row to the top of its group. Omit activeId and both surfaces keep comparing against activeTo as before.

workspace.menuItems, workspace.menuFooter, and the theme toggle are available from the mobile Menu drawer. Use mainLayout/mainClassName when a workspace shell needs the same mobile chrome but a custom main scroll region, such as a full-bleed marketing page. useCommands() works from workspace descendants without manually mounting CommandPalette; lower-level primitive compositions can still mount CommandPalette directly.

ContextRail is usable for both navigation rails and custom rendered rails. Navigation rails show Meda's label header by default. Custom rendered rails hide the automatic visible header by default so consumers can render their own heading without duplication. Rail bodies scroll vertically by default; use contextRail={{ header: "visible" }} or contextRail={{ scroll: "none" }} when you need explicit control.

Foundation primitives

Meda includes small foundation primitives for repeated loading, empty, and filtering surfaces:

import { EmptyState, FilterRail, Skeleton } from '@medalsocial/meda';

Skeleton mirrors shadcn's simple loading placeholder shape. EmptyState standardizes zero/error states across panels and content areas. FilterRail provides a dense filter surface while leaving selected values, URL syncing, and query logic in the consuming app.

The same primitives are available from the shadcn-compatible registry when an app wants local source ownership:

npx shadcn add https://meda.medalsocial.com/r/meda-skeleton.json
npx shadcn add https://meda.medalsocial.com/r/meda-empty-state.json
npx shadcn add https://meda.medalsocial.com/r/meda-filter-rail.json

For app-scoped brand tokens:

import { createMedaThemeCss, defineMedaTheme } from '@medalsocial/meda/theme';

const css = createMedaThemeCss(
  defineMedaTheme({
    appId: 'auto',
    colors: {
      primary: 'var(--hb-brand-500)',
      background: 'var(--hb-base-50)',
    },
    dark: {
      colors: {
        background: 'var(--hb-base-900)',
      },
    },
  })
);

Lower-level ShellStateProvider and layout parts remain available from @medalsocial/meda/shell/primitives for apps that need to own more composition.

See the demo app for a live playground.

Exports

  • @medalsocial/meda — curated public API (components + helpers)
  • @medalsocial/meda/shell — styled shell components and hooks
  • @medalsocial/meda/shell/primitives — lower-level shell state and layout primitives
  • @medalsocial/meda/auth — provider-neutral auth controls
  • @medalsocial/meda/auth/better-auth — optional better-auth adapter
  • @medalsocial/meda/recipes/next — copyable Next.js adoption recipe metadata
  • @medalsocial/meda/theme — app-scoped token bridge helpers
  • @medalsocial/meda/marketing — marketing sections and campaign blocks
  • @medalsocial/meda/primitives — foundation primitives (Button, Input, ToggleGroup, Avatar + getInitials, Card, Skeleton, StatusPill, EmptyState, FilterRail); also re-exported from the root
  • @medalsocial/meda/styles.css — design tokens + base styles + every component's utilities
  • @medalsocial/meda/styles/base.css, @medalsocial/meda/styles/bridge.css, @medalsocial/meda/<feature>/styles.css — lean stylesheet entries (see Lean consumer setup)

Alternative: shadcn registry

Prefer to copy source into your project instead of installing? The shadcn-compatible registry is served alongside the demo playground on Cloudflare Workers.

# Once DNS for meda.medalsocial.com is live:
npx shadcn add https://meda.medalsocial.com/r/meda-shell.json
npx shadcn add https://meda.medalsocial.com/r/meda-shell-state.json
npx shadcn add https://meda.medalsocial.com/r/meda-workbench-layout.json
npx shadcn add https://meda.medalsocial.com/r/meda-next-app-shell.json

The registry index is at https://meda.medalsocial.com/registry.json. Source JSON files live under ./registry in this repo and are deployed as static assets via Cloudflare Workers — see wrangler.toml and .github/workflows/deploy-worker.yml.

Development

pnpm install
pnpm build             # compile dist/
pnpm test              # vitest (jsdom)
pnpm typecheck
pnpm lint
pnpm registry:validate
pnpm demo:dev          # run the playground locally
pnpm storybook         # http://localhost:6006
pnpm storybook:build   # storybook-static/
pnpm size-limit        # bundle-size gate

Storybook + Chromatic

Stories are colocated as Component.stories.tsx next to each primitive. Build the static bundle with:

pnpm storybook:build

Visual review runs via Chromatic. The Chromatic GitHub workflow publishes Storybook on pushes and pull requests targeting dev or prod, using the repository secret CHROMATIC_PROJECT_TOKEN.

pnpm chromatic

Chromatic handles UI diffs and review state, so visual snapshot baselines are not committed to this repository.

Release

Changesets + GitHub Actions (OIDC trusted publishing). Add a changeset with pnpm changeset and merge into dev; the version bump lands on dev by itself, and a standing release: promote dev → prod pull request is kept open for you. Approving that PR is the release — it merges itself and publishes to npm with provenance.

Need a build before then? Run the Release workflow with mode: snapshot for a throwaway prerelease under its own dist-tag.

Full flow, hotfixes and failure handling: CONTRIBUTING.md → Releasing.

License

Apache-2.0. See LICENSE and NOTICE.