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

@vinyasa/patterns

v2.0.3

Published

8 composite page-and-workspace patterns built on top of the rest of `@vinyasa/*`: AppShell, SplitView, InspectorPanel, MultiPanelLayout, DashboardLayout, SettingsLayout, AnalyticsWorkspace, AIWorkspace.

Downloads

584

Readme

@vinyasa/patterns

8 composite page-and-workspace patterns built on top of the rest of @vinyasa/*: AppShell, SplitView, InspectorPanel, MultiPanelLayout, DashboardLayout, SettingsLayout, AnalyticsWorkspace, AIWorkspace.

Installation

pnpm add @vinyasa/patterns @vinyasa/form @vinyasa/icons @vinyasa/layout @vinyasa/navigation @vinyasa/overlay @vinyasa/tokens @vinyasa/typography react react-dom

Unlike most @vinyasa/* packages, this one composes several sibling packages directly rather than just referencing the token contract: @vinyasa/layout (Box/Flex/Grid, in every component), @vinyasa/typography (Heading/Text, for titles and section headers), @vinyasa/form (IconButton) and @vinyasa/icons (Menu/X icons, for AppShell's mobile trigger and InspectorPanel's close button), and @vinyasa/overlay (Sheet, the mobile drawer AppShell renders below its breakpoint). @vinyasa/navigation is also a peer, but for a slot-fill reason rather than a direct import: AppShell's sidebar/header props are typically filled with its Sidebar/TopNavigation, though any ReactNode works. @vinyasa/ai-native is not a peer dependency — AIWorkspace's composer slot is a plain ReactNode, and any composer works; ai-native's PromptEditor is just what the Storybook demo happens to use. Rendering any of these requires a VinyasaProvider (from @vinyasa/tokens) above them in the tree.

No separate stylesheet import is needed — CSS is bundled into each component's own JS entry, with one exception noted under Subpath imports below.

This package has no root export — every component is subpath-only (import { AppShell } from '@vinyasa/patterns/app-shell', never from '@vinyasa/patterns'). A root barrel re-exporting all 8 components would let a bundler tree-shake the unused JS down to just the ones you import, but the CSS side-effect imports the others carry are not eligible for the same tree-shaking (confirmed empirically with both esbuild and Rollup on this monorepo's other packages). Removing the root entry entirely makes that the only possible outcome, not something that depends on your bundler being clever enough to shake it out.

Usage

AppShell

The top-level page frame every screen renders inside: a persistent sidebar rail, a header row, and a content area. Below mobileBreakpoint, the sidebar moves into a Sheet drawer instead of disappearing.

import { AppShell } from '@vinyasa/patterns/app-shell';
import { Sidebar } from '@vinyasa/navigation/sidebar';
import { TopNavigation } from '@vinyasa/navigation/top-navigation';

function App() {
	return (
		<AppShell
			sidebar={<Sidebar header="Acme Cloud" sections={sidebarSections} />}
			header={<TopNavigation items={navItems} />}
		>
			<DashboardPage />
		</AppShell>
	);
}

| Prop | Type | Default | Description | | -------------------- | -------------------------- | -------------- | -------------------------------------------------------------------------------------------- | | sidebar | ReactNode | — | Required. Persistent nav rail; rendered a second, independent time inside the mobile drawer. | | header | ReactNode | — | Rendered across the top of the content area, alongside AppShell's own mobile menu trigger. | | children | ReactNode | — | Required. Page content. | | footer | ReactNode | — | Optional persistent footer region below content. | | mobileBreakpoint | keyof typeof breakpoints | 'md' | Below this, sidebar moves into the mobile drawer. | | mobileSidebarLabel | string | 'Navigation' | Accessible name for the drawer dialog and its trigger button. |

SplitView

A two-pane resizable layout — a fixed-size primary pane and a flexible secondary remainder, divided by a drag handle. Use it for master/detail shapes: an inbox list beside a reading pane, an editor beside a console.

import { SplitView } from '@vinyasa/patterns/split-view';

<SplitView
	primary={<MessageList />}
	secondary={<MessageDetail />}
	resizeHandleLabel="Resize message list"
/>;

| Prop | Type | Default | Description | | --------------------- | ---------------------------- | -------------- | ------------------------------------------------------------------------------------------------ | | primary | ReactNode | — | Required. The fixed-size pane. | | secondary | ReactNode | — | Required. The remaining-space pane. | | primarySize | number | uncontrolled | Controlled width/height (px) of primary. | | defaultPrimarySize | number | 280 | Initial size when uncontrolled. | | onPrimarySizeChange | (size: number) => void | — | Fires on resize. | | minPrimarySize | number | 160 | Resize floor. | | maxPrimarySize | number | 560 | Resize ceiling. | | orientation | 'horizontal' \| 'vertical' | 'horizontal' | 'horizontal': primary left, dragged left-right. 'vertical': primary on top, dragged up-down. |

<SplitView
	orientation="vertical"
	defaultPrimarySize={360}
	primary={<Editor />}
	secondary={<Console />}
/>

InspectorPanel

A single toggleable, resizable auxiliary panel (properties inspector, contextual details) docked to either edge of a layout. Renders nothing at all when closed — no collapsed rail. It owns no trigger of its own; wire a button to open/onOpenChange.

import { InspectorPanel } from '@vinyasa/patterns/inspector-panel';
import Flex from '@vinyasa/layout/flex';

<Flex style={{ height: '100vh' }}>
	<Canvas />
	<InspectorPanel open={open} onOpenChange={setOpen} title={selectedLayer?.name ?? 'No selection'}>
		<LayerProperties layer={selectedLayer} />
	</InspectorPanel>
</Flex>;

| Prop | Type | Default | Description | | ------------------- | ------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------ | | open | boolean | uncontrolled | Controlled open state. | | defaultOpen | boolean | false | Initial open state when uncontrolled. | | onOpenChange | (open: boolean) => void | — | Fires on open/close (including the built-in close button). | | title | ReactNode | — | Panel header title. | | children | ReactNode | — | Required. Panel body. | | size | number | uncontrolled | Controlled width (px). | | defaultSize | number | 320 | Initial width. | | minSize/maxSize | number | 240/480 | Resize bounds. | | side | 'left' \| 'right' | 'right' | Which edge of the panel the drag handle sits on — place the returned pair on the matching side of your own layout. |

MultiPanelLayout

Generalizes SplitView to three-or-more independently resizable panes in a row, each handle transferring width only between its two immediate neighbors — the shape of a code editor's file-tree/editor/output layout.

import { MultiPanelLayout } from '@vinyasa/patterns/multi-panel-layout';

<MultiPanelLayout
	panels={[
		{ key: 'files', content: <FileExplorer />, defaultWidth: 220, minWidth: 160, maxWidth: 360 },
		{ key: 'editor', content: <Editor />, defaultWidth: 680, minWidth: 320 },
		{ key: 'output', content: <Output />, defaultWidth: 320, minWidth: 200, maxWidth: 480 },
	]}
/>;

| Prop | Type | Default | Description | | ------------------- | ------------------------- | ----------------- | ---------------------------------------------------------- | | panels | MultiPanelLayoutPanel[] | — | Required. 3+ panes — for 2 panes, use SplitView instead. | | resizeHandleLabel | string | 'Resize panels' | Accessible label applied to every handle. |

Each MultiPanelLayoutPanel: { key; content; defaultWidth?: 240; minWidth?: 120; maxWidth?: 640 }. Keyboard on each handle: Arrow Left/Right nudge by 16px, Home/End jump to that pane's min/max.

DashboardLayout

The common workspace-overview page shape — a title/description/actions header above a responsive widget grid — meant to sit inside AppShell's content slot.

import { DashboardLayout } from '@vinyasa/patterns/dashboard-layout';
import Button from '@vinyasa/button/button';

<DashboardLayout
	title="Overview"
	description="Revenue and subscription metrics across all workspaces."
	actions={
		<Button variant="primary" size="sm">
			New report
		</Button>
	}
>
	<StatCard label="MRR" value="$48,200" change="+8.2%" />
	<StatCard label="Active subscriptions" value="1,284" change="+34 this month" />
</DashboardLayout>;

| Prop | Type | Default | Description | | ------------- | ---------------------- | ---------------------------------------- | -------------------------------------------- | | title | ReactNode | — | Required. Page title. | | description | ReactNode | — | Optional subtitle under the title. | | actions | ReactNode | — | Rendered as a gapped row. | | children | ReactNode | — | Required. The widget grid's content. | | columns | GridProps['columns'] | 'repeat(auto-fit, minmax(240px, 1fr))' | Column count or raw grid-template-columns. | | gap | GridProps['gap'] | 4 | Grid gap. |

SettingsLayout

The common settings-page shape — a fixed-width nav pane beside the active section's content, stacking above content below the md breakpoint via a plain CSS reflow (no drawer, unlike AppShell).

import { SettingsLayout } from '@vinyasa/patterns/settings-layout';

<SettingsLayout
	title="Settings"
	nav={<SettingsNav activeKey={activeKey} onSelect={setActiveKey} />}
>
	<ProfileSection />
</SettingsLayout>;

| Prop | Type | Default | Description | | ---------- | ----------- | ------- | ---------------------------------------------------------------------------------------- | | title | ReactNode | — | Optional page heading; omit when one already lives elsewhere (e.g. AppShell's header). | | nav | ReactNode | — | Required. Fixed-width settings nav. | | children | ReactNode | — | Required. Active section's content. |

AnalyticsWorkspace

The common analytics-page shape — a filter/toolbar row above main chart/table content, with an optional resizable side panel built on this package's own InspectorPanel. Owns no trigger; wire a button in filters to flip sidePanelOpen.

import { AnalyticsWorkspace } from '@vinyasa/patterns/analytics-workspace';
import Flex from '@vinyasa/layout/flex';
import Button from '@vinyasa/button/button';

<AnalyticsWorkspace
	filters={
		<Flex justify="between">
			<RangePicker />
			<Button size="sm">Export</Button>
		</Flex>
	}
	sidePanel={selectedDay ? <BreakdownPanel day={selectedDay} /> : null}
	sidePanelTitle={selectedDay ? `${selectedDay} · by source` : undefined}
	sidePanelOpen={panelOpen}
	onSidePanelOpenChange={setPanelOpen}
>
	<TrafficChart
		selectedDay={selectedDay}
		onSelectDay={(day) => {
			setSelectedDay(day);
			setPanelOpen(true);
		}}
	/>
</AnalyticsWorkspace>;

| Prop | Type | Default | Description | | ----------------------- | ------------------------- | ------------ | ------------------------------------------------------- | | filters | ReactNode | — | Required. Toolbar row. | | children | ReactNode | — | Required. Main content — chart or table. | | sidePanel | ReactNode | — | Optional side panel content; omit to skip rendering it. | | sidePanelTitle | ReactNode | — | Side panel header title. | | sidePanelOpen | boolean | uncontrolled | Controlled open state. | | onSidePanelOpenChange | (open: boolean) => void | — | Fires on open/close. |

AIWorkspace

The common chat-app shape — a scrollable message thread with a composer docked at the bottom, plus an optional side panel (conversation history, cited sources) built on InspectorPanel, same pattern as AnalyticsWorkspace.

import { AIWorkspace } from '@vinyasa/patterns/ai-workspace';
import { PromptEditor } from '@vinyasa/ai-native/prompt-editor'; // any composer works — ai-native is not a peer dependency
import Button from '@vinyasa/button/button';

<AIWorkspace
	sidePanel={<SourcesPanel sources={sources} />}
	sidePanelTitle="Sources"
	sidePanelOpen={panelOpen}
	onSidePanelOpenChange={setPanelOpen}
	composer={
		<PromptEditor
			value={draft}
			onValueChange={setDraft}
			onSubmit={handleSend}
			leading={
				<Button variant="outline" size="sm" onClick={() => setPanelOpen(!panelOpen)}>
					Sources
				</Button>
			}
			trailing={
				<Button variant="primary" size="sm" disabled={!draft.trim()} onClick={handleSend}>
					Send
				</Button>
			}
		/>
	}
>
	{conversation.map((message, i) => (
		<MessageBubble key={i} message={message} />
	))}
</AIWorkspace>;

| Prop | Type | Default | Description | | ----------------------- | ------------------------- | ------------ | ------------------------------------------------------- | | children | ReactNode | — | Required. Conversation thread; scrolls independently. | | composer | ReactNode | — | Required. Message input, docked at the bottom. | | sidePanel | ReactNode | — | Optional side panel content; omit to skip rendering it. | | sidePanelTitle | ReactNode | — | Side panel header title. | | sidePanelOpen | boolean | uncontrolled | Controlled open state. | | onSidePanelOpenChange | (open: boolean) => void | — | Fires on open/close. |

Other exports

Beyond the 8 components above, two hooks get their own subpath too, since they're useful on their own outside any one component: useMediaQuery (@vinyasa/patterns/use-media-query) and useBreakpoint (@vinyasa/patterns/use-breakpoint). ResizeHandle/useResizablePanel — the drag-resize primitive underlying SplitView, InspectorPanel, and MultiPanelLayout — stay internal-only as of this version: rspack can't build the same module as both a shared internal chunk and its own standalone entry, and every consumer-facing use of resizing already goes through those three components' own props anyway.

Subpath imports

Every component is imported by its own subpath (e.g. @vinyasa/patterns/app-shell) — there is no root @vinyasa/patterns entry, so import { X } from '@vinyasa/patterns' fails to resolve. See "This package has no root export" above for why.

dashboard-layout has no separate stylesheet — it's pure composition of @vinyasa/layout/@vinyasa/typography primitives with no vanilla-extract styles of its own, so there's nothing to bundle beyond what those packages already ship.

Development

From the repository root:

pnpm --filter @vinyasa/patterns build
pnpm --filter @vinyasa/patterns test
pnpm --filter @vinyasa/patterns lint
pnpm storybook   # Patterns/<ComponentName>