UI building blocks
A reference to the reusable components the generated backoffice ships with: the shell (sidebar, header, search, notifications), the primitives (pills, badges, empty states, toasts), and the interactive pieces (confirm dialogs, sheets, detail panels, bulk bars). Names, props, and where each one is wired in the shipped pages.
Reference
The generated backoffice ships a fixed set of reusable components under src/components/. Two folders matter here: src/components/shell/ holds the app frame (sidebar, header, search, menus), and src/components/ui/ holds the primitives (pills, badges, dialogs, sheets, toasts). This page lists the real component names, their key props, and where each one is wired in the shipped pages. Edit these files directly, or ask the AI to change a component and every page that uses it updates at once.
One import path per family
Shell components import from @/components/shell/*. Primitives import from @/components/ui/*. Icons come from a single re-export at @/lib/icons, never straight from the icon package, so the MaShop builder can swap the icon set per project. The Tailwind class merger cn lives at @/lib/utils.
The shell frame
The frame is mounted once in src/app/(backoffice)/layout.tsx. It renders a Sidebar on the left at 900px and up, a sticky Header on top, and a Subnav that appears only on a few route families. Every backoffice page renders inside this frame.
Sidebar
Sidebar (shell/Sidebar.tsx) is the dark 248px rail. It reads its links from @/lib/nav/sidebar-nav (the SIDEBAR_NAV groups), highlights the active route as a white pill, and shows a numeric badge on Orders when you pass an ordersBadge count above zero (it caps the label at 99+). The bottom block shows the signed-in user and a Log out button that calls the server logout action. To add or reorder links, edit sidebar-nav, not this component.
On screens below 900px the same Sidebar renders inside MobileSidebar (shell/MobileSidebar.tsx), a burger-triggered slide-in drawer. There is one nav source of truth; the drawer passes an onNavigate callback so it auto-closes when you tap a link.
Header
Header (shell/Header.tsx) is a server component. It is 60px tall, sticky, with a blurred translucent background. It composes five children: the mobile burger (MobileSidebar), the page search (PageSearch), the theme toggle (ThemeToggle), the notifications popover (NotificationsPopover), and the profile menu (ProfileMenu). It fetches the notifications feed on the server via getNotificationsFeed() and passes it in.
PageSearch
PageSearch (shell/PageSearch.tsx) is the search input in the header. It searches the dashboard's own pages, built from SIDEBAR_NAV plus the sub-nav groups, and routes to the one you pick. Press Cmd+K (or Ctrl+K) to focus it, arrow keys to move the highlight, Enter to navigate, Escape to close. It filters by label or by href and shows up to 8 results. It is page navigation only; it does not search orders, products, or other database rows.
ProfileMenu
ProfileMenu (shell/ProfileMenu.tsx) is the avatar button that opens a dropdown with three items: My profile (routes to /settings/general), Settings (routes to /settings), and Log out. It takes name, email, role, and avatarUrl. When avatarUrl is null it renders the user's initials.
ThemeToggle
ThemeToggle (shell/ThemeToggle.tsx) flips the data-theme attribute on the <html> element between light and dark and stores the choice in localStorage under THEME_STORAGE_KEY. Every color comes from the token layer, so one toggle re-themes the whole UI. If storage is blocked (private browsing) the attribute still flips for the session.
Subnav
Subnav (shell/Subnav.tsx) is the sticky tab bar under the header. It reads subnavForPath() from @/lib/nav/subnavs and renders only on the route families that have a sub-nav; on every other page it renders nothing. The active tab is the longest matching href prefix, so /blog/categories highlights Categories rather than Articles.
NotificationsPopover
NotificationsPopover (shell/NotificationsPopover.tsx) is the bell in the header. It takes an initialItems array, which the server-rendered Header derives from real data (stock, orders, payments, blog comments). There is no fixture feed.
Interactive primitives
These live in src/components/ui/ and are the pieces you reach for when building a page.
ConfirmDialog
ConfirmDialog (ui/ConfirmDialog.tsx) is the modal you use instead of the browser's confirm(). It is a controlled dialog: you own the open boolean and pass onOpenChange. Props include title, description, confirmLabel (default Confirm), cancelLabel (default Cancel), a variant of default or danger (danger paints the confirm button red), and an onConfirm handler that may be async; the dialog closes itself after onConfirm resolves. The action buttons are at least 44px tall.
Never call the browser confirm
The template's rule is to route every destructive confirmation through ConfirmDialog, not window.confirm or alert. The BulkActionBar below does this automatically for any action you mark with a confirm block.
Sheet and ResponsiveSheet
Sheet (ui/Sheet.tsx) is a slide-in panel built on Radix Dialog, with four sides: right (default, full width up to a max on small screens), left, bottom (85vh, rounded top), and top. It exports Sheet, SheetTrigger, SheetContent, SheetClose, SheetTitle, and SheetDescription. Every sheet gets a close X in the top-right corner.
ResponsiveSheet (ui/ResponsiveSheet.tsx) wraps Sheet and picks the side for you by viewport: at 1023px and below it opens as a bottom sheet; above that it uses the side you pass (default right). You give it a trigger node and children.
DetailPanel
DetailPanel (ui/DetailPanel.tsx) is the right-side editing panel used across coupon editing and similar detail flows. It is a controlled panel: you own open and pass onClose, which fires on backdrop click, Escape, or the close X. It takes a title, an optional subtitle, children for the scrollable body, and an optional footer for a save/cancel row. It uses plain fixed positioning, not a Radix Portal: at 1024px and up it is a 340px aside on the right; below that it is a 75vh bottom sheet. Because it skips the Portal there is no focus trap, so wrap destructive save buttons inside a ConfirmDialog when the panel holds a form.
EmptyState
EmptyState (ui/EmptyState.tsx) is the centered placeholder for an empty list. It takes an icon, a title, an optional body, and an optional call to action. If you pass ctaHref it renders a link; if you pass onCtaClick instead it renders a button. The CTA is at least 44px tall.
Pill, GradientPill, LiveBadge
Pill (ui/Pill.tsx) is the small status chip used across products, orders, customers, and more. It takes a variant of ok, warn, danger, info, muted, or ink, and shows a leading dot by default (dot={false} hides it). This is the most-used primitive: it is wired into dozens of pages through wrappers like ProductStatusPill, CustomerSegmentBadge, and StatusBadge.
GradientPill (ui/GradientPill.tsx) and LiveBadge (ui/LiveBadge.tsx) are variants of the pill: GradientPill paints a 135-degree gradient from a tone preset or a custom gradient string, and LiveBadge shows a pulsing dot with a label (default Live). Both are defined and unit-tested but are not mounted in any shipped page as of now; treat them as available parts you can wire in, not surfaces you will find in the UI.
Toast
Toast (ui/Toast.tsx) wraps the sonner library. Mount the Toaster once (it already sits in src/app/layout.tsx), then call the toast API anywhere: toast.ok('Saved'), toast.error('Could not save'), toast.info('Filters reset'), and toast.dismiss(). Toasts appear bottom-center, dark, and dismiss after 2200ms. Import from this module, never from sonner directly, so the styling stays in one place.
Tooltip
Tooltip (ui/Tooltip.tsx) re-exports Radix tooltip parts: TooltipProvider, Tooltip, TooltipTrigger, and TooltipContent. The content renders as a dark chip with an 8px offset from its trigger.
List-table helpers
Three pieces work together to turn a list into a selectable, bulk-editable table.
useRowSelection
useRowSelection (src/hooks/useRowSelection.ts) tracks which rows are selected. Pass it your current rows (each needs an id) and it returns toggle, selectAll, clear, isSelected, selectedIds, count, isAllSelected, and isSomeSelected. Select-all marks only the rows currently loaded, not an entire un-paged dataset.
BulkActionBar
BulkActionBar (shell/BulkActionBar.tsx) is the dark pill bar that floats bottom-center when at least one row is selected; it hides itself when count is 0. You pass count, the selected ids, an actions array, and onDismiss (usually the selection's clear). Each action has a label, an optional icon, a variant (danger paints red), a handler that receives the selected ids, and an optional confirm block. When an action has a confirm, the click routes through ConfirmDialog first; a failing handler surfaces a toast.error. In the shipped pages this bar is wired on the coupons list (coupons/_components/CouponsTable.tsx).
FiltersPopover
FiltersPopover (ui/FiltersPopover.tsx) is a multi-select filter popover. You pass dimensions (each with a key, a label, and options that may carry counts), a controlled value, and an onApply callback; a render-prop child receives the active-filter count so you can style your own trigger with a badge. On Apply it fires a toast shaped like 3 results · 2 filters (results only when you pass resultsCount); Reset clears the draft and toasts Filters reset. The parent owns the URL or query wiring. This component is defined and unit-tested but is not currently wired into any shipped list page; it is a part you can adopt, not a control you will find in the running UI today.
Sister-page tabs
SectionTabs (ui/SectionTabs.tsx) is a tab strip that routes between sibling list pages rather than toggling visual state. When you switch tabs it forwards only four whitelisted query params (period, from, to, filter) to the sister route and drops everything else. Active state matches by exact path for a section root and by prefix for sub-paths, with an optional matchHref override.
Two wrappers preset the tabs. BlogTabs (shell/BlogTabs.tsx) routes between Articles, Categories, and Comments with a custom matcher, and it is mounted on the blog pages via BlogTabsBar. CatalogTabs (shell/CatalogTabs.tsx) presets Products, Categories, and Attributes, but it is not mounted in any shipped page; the catalog pages navigate through the sidebar and the sticky Subnav instead.
The AI assist button
AiButton (ui/AiButton.tsx) is the sparkle button used on content and SEO fields (for example inside the coupon detail panel). It shows a rough ~ N credits hint computed from the payload size and, on click, POSTs a task plus payload to /api/ai/suggest, then hands the result to your onResult callback. On failure it shows the returned error string, or HTTP {status} when there is none. In the template as shipped, /api/ai/suggest returns deterministic placeholder suggestions from lib/ai/mock-responses.ts; the MaShop builder rewires that route to a real model when it specializes your project. Your prompts and generated code are not sent to OpenAI or Anthropic.
Where these come from
Every component on this page is Next.js and TypeScript in your own repository. You own it. Change a prop, restyle a pill, add a shell menu item, or ask the AI to rework a component, and it ships with your code. To see how pages are assembled from these blocks, read Your code: structure; to add a new page that uses them, read Add a page.
