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

shoppergpt-layout

v0.1.1

Published

Lightweight responsive split layout for AI chat + synced results (ShopperGPT-style UIs).

Readme

shoppergpt-layout

Responsive split layout for ShopperGPT-style apps: AI chat on one side, synced results on the other. Handles desktop vs mobile, optional draggable dividers, pane-collapse detection, and divider styling.

Requirements

  • Node.js 18+ (20 recommended)
  • React 18+ (peer dependency)

Install

npm i shoppergpt-layout

Use the same name in imports and in package.json dependencies (it must match the "name" field published with the package — yours may be scoped, e.g. @your-org/shoppergpt-layout, if you fork or republish).

Quick start

1. Give the layout a parent with explicit size (100vh, a flex child, etc.).

2. Pass your results and chat components.

import { ResponsiveSyncedLayout } from 'shoppergpt-layout';

export function ShopperGptScreen() {
  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <ResponsiveSyncedLayout
        results={<YourResultsPanel />}
        chat={<YourChatPanel />}
      />
    </div>
  );
}

3. Inside YourChatPanel / YourResultsPanel, you may use useShopperGptLayoutPane() (see below).

How panes map

| Layout | Prop names | Typical content | |--------|------------|-----------------| | ResponsiveSyncedLayout | results, chat | Results + chat (auto layout) | | Desktop (≥ breakpoint) | chat → left, results → right | Side by side | | Mobile / tablet | results → top, chat → bottom | Stacked, draggable divider |

Default split is 50 / 50 (divider centered). Static desktop dividers stay centered on resize; draggable dividers also start centered.


Example — full App.tsx

Typical setup with breakpoint, draggable desktop divider, and per-platform divider styles. ChatMock / ResultsMock are your own components (see hook usage for inline results when the mobile pane is collapsed).

import { ResponsiveSyncedLayout } from 'shoppergpt-layout';
import { ChatMock } from './components/ChatMock';
import { ResultsMock } from './components/ResultsMock';

export default function App() {
  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <ResponsiveSyncedLayout
        results={<ResultsMock />}
        chat={<ChatMock />}
        desktopMinWidthPx={1024}
        desktopDraggableDivider
        divider={{ style: { background: '#eee' }, className: 'my-divider' }}
        mobileDivider={{
          style: { background: '#00d2be' },
          handleStyle: { width: 38 },
        }}
        desktopDivider={{ style: { background: 'rgba(255, 0, 0, 0.91)' } }}
      />
    </div>
  );
}

| Prop in this example | Effect | |----------------------|--------| | desktopMinWidthPx={1024} | Desktop layout from 1024px container width upward | | desktopDraggableDivider | Desktop split can be dragged (starts at 50 / 50) | | divider | Shared divider overrides (track + class) | | mobileDivider | Teal mobile bar + handle width | | desktopDivider | Desktop separator color |


Integration guide

Step 1 — Choose a layout component

| Use case | Component | |----------|-----------| | Production apps (recommended) | ResponsiveSyncedLayout | | Force mobile layout only | MobileSyncedLayout | | Force desktop layout only | DesktopSyncedLayout |

import { ResponsiveSyncedLayout } from 'shoppergpt-layout';
// or subpaths:
// 'shoppergpt-layout/mobile'
// 'shoppergpt-layout/desktop'
// 'shoppergpt-layout/responsive'

Step 2 — Size the wrapper

The package uses width: 100% and height: 100%. If the parent has no height, the layout collapses.

// Full viewport
<div style={{ width: '100vw', height: '100vh' }}>
  <ResponsiveSyncedLayout … />
</div>

// Inside flex app shell
<main style={{ flex: 1, minHeight: 0 }}>
  <ResponsiveSyncedLayout … />
</main>

Step 3 — Build chat & results as normal React children

Each child should use height: 100% and its own internal scroll (overflow: auto on the scrollable region).

Step 4 — React to mobile “chat-only” (optional)

When the user drags the mobile divider up and hides results, mirror content into chat if your product needs it:

import { useShopperGptLayoutPane } from 'shoppergpt-layout';

function YourChatPanel() {
  const { showResultsInChat, visiblePane, layoutMode } = useShopperGptLayoutPane();

  return (
    <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
      {/* messages… */}
      {showResultsInChat ? (
        <YourResultsSummary variant="inline" />
      ) : null}
    </div>
  );
}

| Hook field | When it helps | |------------|----------------| | showResultsInChat | true when results pane is fully collapsed on mobile | | visiblePane | 'results' | 'chat' | 'both' | | layoutMode | 'mobile' | 'desktop' |

On desktop, showResultsInChat is always false (both panes stay visible).

No extra memoization is required — the hook uses useSyncExternalStore.


Props reference (ResponsiveSyncedLayout)

| Prop | Default | Description | |------|---------|-------------| | results | — | Results / catalog pane (required) | | chat | — | Chat pane (required) | | desktopMinWidthPx | 1024 | Container width at which layout switches to desktop | | useContainerQuery | true | Use ResizeObserver on the layout root; if false, uses window.innerWidth | | desktopDraggableDivider | false | true = draggable vertical divider on desktop | | desktopInitialLeftRatio | 0.5 | Initial left width ratio (0–1); 0.5 = centered | | mobileInitialTopRatio | 0.5 | Initial top height ratio (0–1); 0.5 = centered | | dividerThicknessPx | — | Override divider thickness (see defaults below) | | theme | dark defaults | background, surface, divider, handle | | divider | — | CSS overrides for both platforms | | mobileDivider | — | Divider CSS overrides (mobile only) | | desktopDivider | — | Divider CSS overrides (desktop only) | | renderDivider | — | Fully custom divider component | | className / style | — | On the outer layout root |

Desktop divider behavior

| desktopDraggableDivider | Thickness (default) | Split | Handle | |---------------------------|----------------------|-------|--------| | false (default) | 1px line | 50 / 50, fixed (re-centers on resize) | None | | true | 12px | 50 / 50 on mount, user can drag | Yes |

<ResponsiveSyncedLayout
  results={<Results />}
  chat={<Chat />}
  desktopDraggableDivider
/>

Mobile divider behavior

  • Always draggable.
  • Default thickness 28px (teal bar style in Spoticar-like UIs).
  • Initial split 50 / 50.
  • Drag to top → chat only (showResultsInChat === true).
  • Drag to bottom → results only.

Override mins if you want to prevent full collapse:

<MobileSyncedLayout
  top={<Results />}
  bottom={<Chat />}
  minTopPx={80}
  minBottomPx={120}
/>

Divider styling

Pass CSS via divider, or split by platform on ResponsiveSyncedLayout:

<ResponsiveSyncedLayout
  results={<Results />}
  chat={<Chat />}
  desktopDivider={{
    style: { background: 'rgba(0, 0, 0, 0.08)' },
  }}
  mobileDivider={{
    style: { background: '#00d2be' },
    className: 'my-mobile-divider',
    handleStyle: { width: 38, height: 4, borderRadius: 2 },
  }}
/>

| DividerCustomization field | Target | |-----------------------------|--------| | style / className | Divider track | | handleStyle / handleClassName | Handle pill (draggable dividers only) |

Custom divider entirely:

<ResponsiveSyncedLayout
  renderDivider={({ onPointerDown, draggable, orientation, thicknessPx, theme, customization }) => (
    <div
      onPointerDown={draggable ? onPointerDown : undefined}
      style={{ flex: `0 0 ${thicknessPx}px`, ...customization?.style }}
    />
  )}
  …
/>

Explicit layouts (no responsive switch)

Desktop — chat left, results right:

import { DesktopSyncedLayout } from 'shoppergpt-layout/desktop';

<DesktopSyncedLayout
  left={<Chat />}
  right={<Results />}
  resizable={false}
/>

Mobile — results top, chat bottom:

import { MobileSyncedLayout } from 'shoppergpt-layout/mobile';

<MobileSyncedLayout top={<Results />} bottom={<Chat />} />

Same defaults: 50 / 50 split, useShopperGptLayoutPane() available inside children.


Local development

Option A — file: symlink (day-to-day)

{
  "dependencies": {
    "shoppergpt-layout": "file:../shoppergpt-layout"
  }
}

| Action | Required after code change? | |--------|------------------------------| | npm install (once) | Symlink only — no reinstall for normal edits | | npm run build in package (or npm run dev watch) | Yes — imports resolve to dist/ | | Reinstall in consumer | Only if path/version breaks or node_modules is corrupted |

Option B — npm pack (production-like)

cd shoppergpt-layout && npm run build && npm pack
cd ../your-app && npm i ../shoppergpt-layout/shoppergpt-layout-0.1.0.tgz

Rebuild, repack, and reinstall the .tgz after every change.


Exports

| Import path | Contents | |-------------|----------| | shoppergpt-layout | All layouts, useShopperGptLayoutPane, types | | shoppergpt-layout/responsive | ResponsiveSyncedLayout | | shoppergpt-layout/mobile | MobileSyncedLayout | | shoppergpt-layout/desktop | DesktopSyncedLayout |

Types: LayoutPaneState, LayoutTheme, DividerCustomization, DividerRenderProps, ResponsiveLayoutProps, etc.


Package scripts

npm run build      # dist + .d.ts
npm run dev        # tsup --watch
npm run typecheck

Troubleshooting

| Problem | Likely cause | Fix | |---------|--------------|-----| | Layout height is 0 | Parent has no height | Set height on wrapper (100vh, flex: 1, etc.) | | Hook throws “must be used inside…” | Component rendered outside layout tree | Call hook only inside chat / results descendants | | Changes not visible in consumer | dist/ stale | Run npm run build in package (or watch mode) | | Always desktop on mobile | Container wider than desktopMinWidthPx | Lower breakpoint or check parent width | | Divider not centered | Custom *Initial*Ratio | Omit props to use default 0.5 |


Design notes

  • Panes stay mounted when collapsed; the layout only changes geometry.
  • Container queries (default) let the layout work inside dashboards, modals, or resizable panels — not only full viewport.
  • Product logic (e.g. copying results into assistant messages) stays in your app; this package exposes layout state via the hook only.