@shoppexio/ui
v0.1.20
Published
Storefront React primitives and components for building Shoppex storefronts
Readme
@shoppex/ui
Storefront React primitives, components, and blocks for building Shoppex storefronts. Works seamlessly with the Storefront SDK.
Scope check (read this if you are an agent or new contributor):
This package is the storefront-SDK primitive library — it provides hooks (
useCart,useStore,useCheckout), composed components (ProductCard,CartDrawer,CheckoutModal,SearchCommand), and blocks (Hero,ProductGrid,FAQ,Footer) for theme authors and external SDK consumers building on top of@shoppexio/storefront.This is NOT the shared design system for internal apps. The merchant dashboard (
apps/dashboard), checkout (apps/checkout), and customer-portal (apps/customer-portal) each maintain their own localcomponents/ui/directories with shadcn-style primitives. Those apps are not expected to import from@shoppex/uibeyond the already-used@shoppex/ui/utilshelpers in the dashboard's code editor.If you are consolidating internal UI primitives across apps, that is a separate (valid) initiative — track it explicitly instead of moving code into this package.
Installation
bun add @shoppex/ui@npm:@shoppexio/ui @shoppexio/storefront@shoppex/ui is the internal import name used by first-party themes. The published npm package is @shoppexio/ui; themes install it through the npm alias above so source imports stay stable while release packages use the public scope.
Quick Start
import { shoppex } from '@shoppexio/storefront';
import { useStore, useCart, ProductGrid, CartDrawer, Hero } from '@shoppex/ui';
// Initialize SDK
shoppex.init('your-store-slug');
function App() {
const { store, products, isLoading } = useStore();
const { totalQuantity } = useCart();
const [cartOpen, setCartOpen] = useState(false);
if (isLoading) return <ProductGridSkeleton count={8} />;
return (
<div>
<Hero store={store} products={products} />
<ProductGrid products={products} />
<CartDrawer isOpen={cartOpen} onClose={() => setCartOpen(false)} />
</div>
);
}What's Included
Primitives (Hooks)
| Hook | Description |
|------|-------------|
| useCart() | Cart state + add/remove/update operations |
| useCartItem(productId) | Cart state for a specific product |
| useStore() | Fetch store, products, groups |
| useStoreSettings() | Store configuration (cart enabled, etc.) |
| useSearch() | Product search with debouncing |
| useProductFilter() | Filter/sort products by category |
| useCheckout() | Checkout flow + coupon validation |
| useCartDealQuote() | Server-quoted cart deals, discounts, and bundle suggestions |
| usePrice() | Price formatting with store currency |
| useProductPricing() | Product pricing details (discount, subscription) |
Components
| Component | Description |
|-----------|-------------|
| ProductCard | Product card with image, price, add-to-cart |
| AddToCartButton | Smart button with quantity controls |
| CartDrawer | Slide-over cart sidebar |
| CheckoutModal | Email collection modal |
| SearchCommand | Cmd+K search palette |
| SearchBar | Search trigger button |
| PriceDisplay | Formatted price with discount/subscription |
| QuantityStepper | +/- quantity input |
| CouponInput | Coupon code input with validation |
Blocks (Sections)
| Block | Description |
|-------|-------------|
| Hero | Store hero with logo, title, stats |
| ProductGrid | Responsive product grid with filters |
| WhyChooseUs | Features/trust bento grid |
| FAQ | Accordion FAQ section |
| Footer | Store footer with links |
Styling
Components use CSS variables for theming. Define these in your CSS:
:root {
--color-background: #09090b;
--color-foreground: #fafafa;
--color-card: #18181b;
--color-primary: #7c3aed;
--color-primary-foreground: #ffffff;
--color-muted: #27272a;
--color-muted-foreground: #a1a1aa;
--color-border: #27272a;
--color-destructive: #ef4444;
}Or use Tailwind CSS with these variables configured.
Using with React Router
Components accept a LinkComponent prop for custom link handling:
import { Link } from 'react-router-dom';
<ProductCard
product={product}
href={`/product/${product.slug}`}
LinkComponent={({ href, children, className }) => (
<Link to={href} className={className}>{children}</Link>
)}
/>
<CartDrawer
isOpen={isOpen}
onClose={() => setIsOpen(false)}
checkoutHref="/checkout"
LinkComponent={({ href, children, className, onClick }) => (
<Link to={href} className={className} onClick={onClick}>{children}</Link>
)}
/>SSR Support
Use InitialDataProvider for SSR/SSG:
import { InitialDataProvider } from '@shoppex/ui';
// Server-side
const { store, products } = await fetchInitialData();
// Client
<InitialDataProvider data={{ store, products }}>
<App />
</InitialDataProvider>Examples
Custom Product Card
<ProductCard
product={product}
href={`/product/${product.slug}`}
showRating={false}
showDescription={false}
aspectRatio="square"
renderActions={(p) => (
<button onClick={() => quickView(p)}>Quick View</button>
)}
/>Search with Cmd+K
function App() {
const [searchOpen, setSearchOpen] = useState(false);
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
setSearchOpen(true);
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, []);
return (
<>
<SearchBar onClick={() => setSearchOpen(true)} />
<SearchCommand
isOpen={searchOpen}
onClose={() => setSearchOpen(false)}
getProductHref={(p) => `/product/${p.slug}`}
/>
</>
);
}Custom Checkout Flow
const { checkout, validateCoupon } = useCheckout();
// Validate coupon
const result = await validateCoupon('SAVE10');
if (result.valid) {
console.log(`${result.discount}% off!`);
}
// Custom checkout (no auto-redirect)
const checkoutResult = await checkout({
email: '[email protected]',
coupon: 'SAVE10',
autoRedirect: false,
});
if (checkoutResult.success) {
// Track event, then redirect
analytics.track('checkout_started');
window.location.href = checkoutResult.redirectUrl;
}License
MIT
