templates/basic/.claude/skills/dashboard-page-ui/SKILL.md
Create or modify pages in admin or vendor dashboards in the Mercur basic starter using correct routing, page composition, i18n, and dashboard UI conventions.
npx skillsauth add mercurjs/mercur dashboard-page-uiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill when:
apps/admin/src/routes or apps/vendor/src/routesApplies equally to admin and vendor dashboards — they share the same component library and routing conventions.
src/routes (NOT src/pages — the mercurDashboardPlugin scans src/routes only).medusa-ui-conformance.dashboard-form-ui.dashboard-tab-ui.src/pages — the mercurDashboardPlugin scans src/routes only.For a page to appear in the sidebar, its page.tsx must export a named config:
import type { RouteConfig } from "@mercurjs/dashboard-sdk";
import { Star } from "@medusajs/icons";
export const config: RouteConfig = {
label: "My Page",
icon: Star,
};
Without this export the page is reachable by URL but invisible in navigation.
Use _DataTable, SingleColumnPage, and useDataTable from @mercurjs/dashboard-shared.
A list page should include:
SingleColumnPage wrapperContainer with heading and action buttons (Create via Link to the create route)_DataTable with pagination, search, filters, orderBy, count, pageSize, noRecordsnavigateTo on _DataTable to link rows to detail pagesDetail pages use TwoColumnPage with .Main and .Sidebar from @mercurjs/dashboard-shared.
<TwoColumnPage data={entity} showJSON showMetadata>
<TwoColumnPage.Main>
<Container className="divide-y p-0">
{/* Header: title, StatusBadge for status, ActionMenu for actions */}
<SectionRow title="Field Label" value={entity.field} />
</Container>
{/* Group related fields into separate Container blocks with Heading level="h2" */}
</TwoColumnPage.Main>
<TwoColumnPage.Sidebar>
<Container className="divide-y p-0">
{/* Secondary info: organization, inventory, metadata */}
<SectionRow title="Field Label" value={entity.field} />
</Container>
</TwoColumnPage.Sidebar>
</TwoColumnPage>
SectionRow for key-value pairs — title is the label, value is string or ReactNode.StatusBadge only for statuses (published, draft, active, etc.) — never Badge.Badge.ActionMenu with groups for contextual actions (Edit, Delete). Put destructive actions in a separate group.Create pages live at src/routes/<entity>/create/page.tsx and open as a RouteFocusModal full-screen overlay:
const CreatePage = () => (
<RouteFocusModal>
<CreateForm />
</RouteFocusModal>
);
export default CreatePage;
The form component inside uses useRouteModal() for handleSuccess. See dashboard-form-ui or dashboard-tab-ui for form details.
Edit pages use RouteDrawer — a side panel that slides in from the right over the detail page.
Critical: The edit directory must use the @ prefix to create a parallel route:
src/routes/<entity>/[id]/@edit/page.tsxsrc/routes/<entity>/[id]/edit/page.tsxWithout @, the SDK creates a flat route that replaces the detail page — the background disappears. With @, it creates a child route that renders inside the parent's Outlet.
The detail page must set hasOutlet to render the child route:
<TwoColumnPage data={entity} showJSON showMetadata hasOutlet>
Edit drawer structure:
const EditForm = () => {
const { handleSuccess } = useRouteModal();
const form = useForm({ ... });
const handleSubmit = form.handleSubmit(async (data) => {
handleSuccess(`/<entity>/${id}`);
});
return (
<RouteDrawer.Form form={form}>
<form onSubmit={handleSubmit} className="flex h-full flex-col overflow-hidden">
<RouteDrawer.Header>
<RouteDrawer.Title>Edit Entity</RouteDrawer.Title>
<RouteDrawer.Description>Update details.</RouteDrawer.Description>
</RouteDrawer.Header>
<RouteDrawer.Body className="flex flex-col gap-y-4 overflow-y-auto p-6">
{/* Form.Field components */}
</RouteDrawer.Body>
<RouteDrawer.Footer>
<RouteDrawer.Close asChild>
<Button size="small" variant="secondary">{t("actions.cancel")}</Button>
</RouteDrawer.Close>
<Button size="small" variant="primary" type="submit">{t("actions.save")}</Button>
</RouteDrawer.Footer>
</form>
</RouteDrawer.Form>
);
};
const EditPage = () => (
<RouteDrawer>
<EditForm />
</RouteDrawer>
);
export default EditPage;
Link to edit from the detail page ActionMenu:
{ label: "Edit", icon: <PencilSquare />, onClick: () => navigate(`/<entity>/${id}/edit`) }
page.tsx controls the route.config: RouteConfig when the page should appear in navigation.src/routes/<entity>/[id]/page.tsxsrc/routes/<entity>/create/page.tsxsrc/routes/<entity>/[id]/@edit/page.tsx (note @ prefix)documentation
Analyze a Mercur 1.x project and guide migration to 2.0. Self-contained — works without access to the mercur monorepo.
documentation
Plan and execute migration from Mercur 1.x to 2.0. Classifies project difficulty, reads relevant migration docs, and follows stop conditions.
development
Review code changes for contract compliance, type safety, and regression risk. Use after completing any non-trivial implementation, before merging PRs, or when asked to review code quality across any mercur package.
tools
Use Mercur CLI commands correctly inside a project created from the Mercur basic starter. Use when choosing between `create`, `init`, `add`, `search`, `view`, and `diff`.