shoppergpt-layout
v0.1.1
Published
Lightweight responsive split layout for AI chat + synced results (ShopperGPT-style UIs).
Maintainers
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-layoutUse 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.tgzRebuild, 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 typecheckTroubleshooting
| 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.
