@mercurjs/docs 2.2.0-canary.40 → 2.2.0-canary.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -77,7 +77,7 @@ The [Mercur CLI](/rc/tools/cli) (`@mercurjs/cli@rc`) manages your project — sc
77
77
 
78
78
  ## Dashboard SDK
79
79
 
80
- The [Dashboard SDK](/rc/tools/dashboard-sdk) (`@mercurjs/dashboard-sdk`) is a Vite plugin shared by the admin panel and vendor portal. It provides file-based routing, automatic navigation generation, component overrides, and i18n support.
80
+ The [Dashboard SDK](/rc/tools/dashboard-sdk) (`@mercurjs/dashboard-sdk`) is a Vite plugin shared by the admin panel and vendor portal. It provides file-based routing, automatic navigation generation, widget and custom-field extensions, and i18n support.
81
81
 
82
82
  ## API Client
83
83
 
@@ -9,6 +9,10 @@ Custom Fields let you attach extra data to any existing Medusa entity — produc
9
9
  **Where this sits in the stack.** Custom Fields is a Mercur module (`@mercurjs/core/modules/custom-fields`) layered on top of Medusa's standard module-link system — it generates the side table, the link, and the schema for you. It's the lightest way to add data to an entity.
10
10
  </Info>
11
11
 
12
+ <Note>
13
+ **This is the *data* layer — not the panel UI.** This module *stores* extra data. The panel-side `defineCustomFieldsConfig` helper (see [Extend forms and tables](/rc/resources/tutorials/extend-forms-and-tables)) *renders* fields into the built-in forms, sections, and tables. They share a name but solve different halves of the problem: use this module for storage and queryability, and the panel helper to expose the values to admins and vendors.
14
+ </Note>
15
+
12
16
  ## When to use Custom Fields
13
17
 
14
18
  <Tip>
@@ -163,7 +167,7 @@ import {
163
167
  Yes — edit the `customFields` config and run `db:migrate` again; the module updates the side table's schema. Removing a field from config stops exposing it; treat destructive column changes with the same care as any schema migration.
164
168
  </Accordion>
165
169
  <Accordion title="How do vendors or admins edit these values from the panels?">
166
- Through your own UI: add a page or re-compose an existing one ([extending panels](/rc/resources/customization/extending-panels)) and call an API route that uses the service's `upsert` — or compose `upsertCustomFieldsStep` into a workflow behind a [custom route](/rc/resources/tutorials/custom-api-route).
170
+ Through your own UI: add a page or a [panel custom field](/rc/resources/customization/extending-panels#custom-fields) and call an API route that uses the service's `upsert` — or compose `upsertCustomFieldsStep` into a workflow behind a [custom route](/rc/resources/tutorials/custom-api-route).
167
171
  </Accordion>
168
172
  <Accordion title="Can one entity have multiple custom-field records?">
169
173
  No — the side table has a unique constraint on the parent ID, enforcing one-to-one. If you need many records per parent, that's a custom module, not custom fields.
@@ -1,32 +1,43 @@
1
1
  ---
2
2
  title: "Extending Panels"
3
- description: "Add pages, customize navigation, branding, and replace components in the admin and vendor panels."
3
+ description: "Add pages, inject widgets, customize navigation, and extend forms and tables in the admin and vendor panels."
4
4
  ---
5
5
 
6
- Both the admin panel and vendor portal use the same SDK (`@mercurjs/dashboard-sdk`). Customization is file-based and convention-driven — you add pages by creating files, configure navigation through exports, and override components through config.
6
+ Both the admin panel and vendor portal use the same SDK (`@mercurjs/dashboard-sdk`). Customization is **file-based and convention-driven** — you add pages, widgets, and field extensions by dropping files under `src/`, and configure navigation through a single file. Nothing is registered by hand; the SDK crawls your `src/` at build time and wires everything in.
7
7
 
8
8
  <Info>
9
- **Mercur does not use Medusa's admin customization.** If you're coming from Medusa, forget `defineWidgetConfig`, `defineRouteConfig`, and widget injection zones none of them exist here. Mercur ships its own admin **and** vendor panels, built on a shared extension framework with three mechanisms: **drop-in routes** (file-based pages that can also *replace* built-in pages), **compound-component re-composition** (every built-in page exposes its parts as slots), and **named layout overrides** (sidebar, topbar, store setup). The trade: instead of injecting widgets into fixed zones around a page Medusa controls, you compose the actual pagewhich is what makes a two-panel marketplace customizable without forking.
9
+ **Coming from Medusa?** The extension model is deliberately Medusa-shaped. The helpers you know `defineWidgetConfig`, `defineRouteConfig`, `defineCustomFieldsConfig`, `createFormHelper`all exist here, re-exported from `@mercurjs/dashboard-sdk` (without Medusa's `unstable_` prefix). What's different: Mercur ships **two** panels (admin *and* vendor) from one framework, so a file only ever targets the panel it lives in (there is no `surface` field), and widget zones carry a `before | after` placement suffix on the zone id. Everything is additive by defaultyour contribution augments the built-in page instead of replacing it.
10
10
  </Info>
11
11
 
12
- ## Choosing your extension mechanism
12
+ ## The extension mechanisms
13
13
 
14
- Junior-friendly rule of thumb: **adding something new? Drop in a route. Changing something that exists? Re-compose it. Changing the frame around every page? Override a component. Sharing it across projects? Make it a block.**
14
+ Every panel customization is **additive** you augment a built-in page without owning it. One concern per file, discovered by the build-time crawl:
15
15
 
16
- | You want to… | Use | Why it's the right tool |
17
- |--------------|-----|------------------------|
18
- | Add a new screen or feature | A drop-in `page.tsx` route | New URL, auto-registered, sidebar entry via the `config` export — see [Add a page](#add-a-page) |
19
- | Change part of an existing page | A drop-in route **at the same path**, re-composing the page's compound slots | Your route replaces the built-in one; untouched slots keep all their behavior — see the [re-compose tutorial](/rc/resources/tutorials/recompose-a-page) |
20
- | Change global chrome (sidebar, topbar, store setup) | A component override in the plugin config | One component, applied everywhere its slot renders — see [Replace components](#replace-components) |
21
- | Reuse a feature across projects | A [block](/rc/learn/blocks) | Ships API + admin + vendor files installable with `mercurjs add` — see the [build-a-block tutorial](/rc/resources/tutorials/build-a-block) |
16
+ - **Widgets** inject a React component at a named zone on a built-in page (`defineWidgetConfig`).
17
+ - **Navigation** — reorder, hide, relabel, or re-parent built-in sidebar items (`defineNavigationConfig`).
18
+ - **Custom fields** add validated fields to built-in forms, replace/remove/add fields in detail sections, and add columns to list tables (`defineCustomFieldsConfig` + `createFormHelper`).
19
+ - **Pages** add a brand-new screen with a drop-in `page.tsx` route.
22
20
 
23
21
  <Note>
24
- The middle row is the one Medusa developers miss most. Because built-in pages are exported as compound components (e.g. `ProductListPage` from `@mercurjs/vendor/pages` with `Header`, `HeaderActions`, `DataTable` slots), "customize the products page" doesn't mean forking it — it means rendering it with your own composition and reusing the slots you don't change.
22
+ Additive tools leave the rest of the built-in page data fetching, filters, pagination, i18n completely intact. A widget, a nav override, or a custom field changes only the spot you target.
25
23
  </Note>
26
24
 
25
+ ## Choosing your extension mechanism
26
+
27
+ Rule of thumb: **injecting UI into an existing page? Use a widget. Adding data to a form or section? Use a custom field. Reshaping the sidebar? Use the navigation file. Adding a whole new screen? Drop in a route.**
28
+
29
+ | You want to… | Use | Why it's the right tool |
30
+ |--------------|-----|------------------------|
31
+ | Show a banner, panel, or CTA on a built-in page | A **widget** (`defineWidgetConfig`) | Renders at a named zone; the rest of the page is untouched — see [Add a widget](/rc/resources/tutorials/add-a-widget) |
32
+ | Add a field to a built-in form, or add/replace/remove a field in a detail section | A **custom field** (`defineCustomFieldsConfig`) | Validated field wired into the existing form/section — see [Extend forms and tables](/rc/resources/tutorials/extend-forms-and-tables) |
33
+ | Add or override a column on a built-in list table | The `list` block of a **custom field** file | Model-scoped column extension — see [Extend forms and tables](/rc/resources/tutorials/extend-forms-and-tables) |
34
+ | Reorder / hide / relabel / re-parent sidebar items | The **navigation** file (`defineNavigationConfig`) | One host-owned `_navigation.ts` — see [Customize navigation](/rc/resources/tutorials/customize-navigation) |
35
+ | Add a new screen or feature | A drop-in `page.tsx` route | New URL, auto-registered, sidebar entry via `config` — see [Add a page](/rc/resources/tutorials/custom-panel-page) |
36
+ | Reuse a feature across projects | A [block](/rc/learn/blocks) | Ships API + admin + vendor files installable with `mercurjs add` |
37
+
27
38
  ## Set up
28
39
 
29
- All configuration lives in the panel app's Vite config — there is no separate config file.
40
+ All configuration lives in the panel app's Vite config — there is no separate `mercur.config.ts`.
30
41
 
31
42
  <Steps>
32
43
  <Step title="Register the plugin">
@@ -54,8 +65,17 @@ All configuration lives in the panel app's Vite config — there is no separate
54
65
  <Step title="Pass environment values explicitly">
55
66
  The plugin doesn't read `.env` itself — load environment variables in `vite.config.ts` (e.g. with Vite's `loadEnv`) and pass them into the plugin options, as the starter template does with `backendUrl`.
56
67
  </Step>
68
+ <Step title="Register typed extension targets (once per panel)">
69
+ Widgets, navigation, and custom fields target **typed ids** (zone ids, nav item ids, model field ids) that each panel package generates from its own built-in pages and ships as `@mercurjs/{admin,vendor}/extension-targets`. Register them once so the ids resolve in every extension file, with a single ambient reference in your app's `src`:
70
+
71
+ ```typescript apps/vendor/src/extension-targets.d.ts
72
+ /// <reference types="@mercurjs/vendor/extension-targets" />
73
+ ```
74
+
75
+ (For the admin app, reference `@mercurjs/admin/extension-targets`.) With this file present, a typo in a zone, model, field, or nav id fails `tsc` instead of silently doing nothing at runtime. `create-mercur-app` ships this file already.
76
+ </Step>
57
77
  <Step title="Restart after config changes">
58
- Options are applied at build time through virtual modules. Adding or removing routes hot-reloads automatically, but changes to the plugin options require a dev-server restart.
78
+ Options are applied at build time through virtual modules. Adding or removing routes, widgets, and custom-field files hot-reloads automatically, but changes to the plugin options (in `vite.config.ts`) require a dev-server restart.
59
79
  </Step>
60
80
  </Steps>
61
81
 
@@ -68,11 +88,127 @@ All configuration lives in the panel app's Vite config — there is no separate
68
88
  | `vendorUrl` | `string` | Absolute vendor portal URL including its path prefix |
69
89
  | `name` | `string` | Application name shown in the sidebar |
70
90
  | `logo` | `string` | URL to a logo image |
71
- | `components` | `object` | Component overrides (see [Replace components](#replace-components)) |
72
91
  | `i18n` | `object` | Internationalization settings (`{ defaultLanguage }`) |
73
92
  | `enableSellerRegistration` | `boolean` | Enable the public seller registration flow (vendor portal) |
74
93
  | `imageLimit` | `number` | Max upload size for images in bytes (default: 2 MB) |
75
94
 
95
+ ## Widgets
96
+
97
+ Inject a React component at a named zone on a built-in page. Drop a file under `src/widgets/`, export the component as the default and a `config` built with `defineWidgetConfig`:
98
+
99
+ ```tsx src/widgets/product-list-banner.tsx
100
+ import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
101
+ import { Container, Text } from "@medusajs/ui"
102
+
103
+ export const config = defineWidgetConfig({
104
+ zone: "product.list.before",
105
+ })
106
+
107
+ export default function ProductListBanner() {
108
+ return (
109
+ <Container className="mb-2">
110
+ <Text size="small" className="text-ui-fg-subtle">
111
+ Tip: bulk-import products from the Products menu.
112
+ </Text>
113
+ </Container>
114
+ )
115
+ }
116
+ ```
117
+
118
+ The zone id is `<domain>.<view>.<placement>`. The placement is the last segment:
119
+
120
+ - `before` / `after` — stack your widget before or after the built-in content (multiple widgets stack in registration order).
121
+
122
+ Zones mounted today (**vendor portal**):
123
+
124
+ | Zone id | Where it renders |
125
+ |---------|------------------|
126
+ | `product.list.before` / `.after` | Vendor product list page |
127
+ | `login.logo.*` / `login.before.*` / `login.after.*` | The public login screen (rendered before authentication) |
128
+
129
+ The full set of valid zones is typed as `WidgetZoneId` and generated into each panel's `extension-targets.d.ts` — your editor autocompletes them, and an unknown zone fails `tsc`. Walk through it end to end in [Add a widget](/rc/resources/tutorials/add-a-widget).
130
+
131
+ ## Navigation
132
+
133
+ Custom drop-in routes place their own sidebar item via `defineRouteConfig` (see [Add a page](#add-a-page)). To reshape the **built-in** sidebar items, author a single host-owned file, `src/_navigation.ts`:
134
+
135
+ ```ts src/_navigation.ts
136
+ import { defineNavigationConfig } from "@mercurjs/dashboard-sdk"
137
+
138
+ export default defineNavigationConfig({
139
+ items: [
140
+ { id: "orders", rank: 0 }, // pin to the top
141
+ { id: "price-lists", hidden: true }, // hide a built-in item
142
+ { id: "payouts", label: "Settlements" }, // relabel
143
+ { id: "campaigns", nested: "orders" }, // re-parent under Orders
144
+ { id: "categories", nested: null, rank: 1 }, // promote a nested item to top level
145
+ ],
146
+ })
147
+ ```
148
+
149
+ - `id` targets any built-in item — top-level routes *or* nested children — by its stable id, typed as `NavItemId`.
150
+ - `rank` orders an item within its parent; `hidden` removes it from the sidebar; `label` / `icon` relabel it; `nested` re-parents it (`nested: null` promotes to top level, typed against `NavParentId`).
151
+ - Navigation is a **single host-owned file** — installed blocks cannot reorder the sidebar, so it stays one source of truth. It does not change custom routes, which still place themselves via `defineRouteConfig`.
152
+
153
+ Walk through it in [Customize navigation](/rc/resources/tutorials/customize-navigation).
154
+
155
+ ## Custom fields
156
+
157
+ Add validated fields to a model's built-in create/edit forms, replace/remove/add fields in its detail sections, and add columns to its list table — all from one model-scoped file. Drop `src/custom-fields/<model>.tsx` and default-export a `defineCustomFieldsConfig`:
158
+
159
+ ```tsx src/custom-fields/product.tsx
160
+ import { defineCustomFieldsConfig } from "@mercurjs/dashboard-sdk"
161
+ import { createFormHelper } from "@mercurjs/dashboard-shared"
162
+ import { Text } from "@medusajs/ui"
163
+
164
+ const form = createFormHelper<{ metadata?: Record<string, unknown> }>()
165
+
166
+ export default defineCustomFieldsConfig({
167
+ model: "product",
168
+ forms: [
169
+ {
170
+ zone: "edit",
171
+ fields: {
172
+ erp_id: form.define({
173
+ validation: form.string().optional(), // Zod → input type + validation
174
+ label: "ERP ID",
175
+ description: "External system identifier",
176
+ placeholder: "ERP-000",
177
+ }),
178
+ },
179
+ },
180
+ ],
181
+ displays: [
182
+ {
183
+ zone: "general",
184
+ fields: [
185
+ { id: "erp_id", component: ({ data }) => <Text className="px-6 py-4">ERP: {String(data.metadata?.erp_id ?? "-")}</Text> }, // ADD
186
+ { id: "subtitle", component: null }, // REMOVE a built-in field
187
+ { id: "handle", component: BrandedHandle }, // REPLACE a built-in field's render
188
+ ],
189
+ },
190
+ ],
191
+ list: {
192
+ columns: [{ id: "erp_id", header: "ERP", component: ({ row }) => String(row.metadata?.erp_id ?? "-") }],
193
+ viewDefaults: {
194
+ columnVisibility: { collection: false }, // hide a built-in column
195
+ columnOrder: ["product", "erp_id", "status"],
196
+ },
197
+ },
198
+ })
199
+ ```
200
+
201
+ - `forms[]` adds validated fields to a form `zone` (`create` / `edit` / `organize` / …). Input type and validation come from a Zod schema via `createFormHelper`; fields render through the standard `Form.Field` chain and participate in the existing submit + validation flow.
202
+ - `displays[]` targets detail-page sections: keyed by field `id`, an entry **adds** a read-only row (unknown id), **replaces** a built-in field's render (matching id + `component`), or **removes** it (matching id + `component: null`).
203
+ - `list` extends the model's list table: add/override columns by id, hide via `viewDefaults.columnVisibility`, reorder via `viewDefaults.columnOrder`.
204
+ - Everything is typed against the panel-generated `CustomFieldsRegistry` — valid `model`, `zone`, and built-in field ids autocomplete; an invalid target fails `tsc`.
205
+
206
+ <Warning>
207
+ **Panel custom fields vs. the Custom Fields module.** `defineCustomFieldsConfig` (this section) is a **UI** surface — it renders, validates, and displays fields in the panels. It does not create database columns. To *store* extra data on an entity, use the backend [Custom Fields module](/rc/resources/customization/custom-fields), or wire your own API route/workflow. In the MVP, panel custom fields for `product` are submitted under `additional_data` and persisted onto the product's `metadata`.
208
+ </Warning>
209
+
210
+ Full walkthrough (forms, sections, list columns): [Extend forms and tables](/rc/resources/tutorials/extend-forms-and-tables).
211
+
76
212
  ## Add a page
77
213
 
78
214
  <Steps>
@@ -114,7 +250,7 @@ All configuration lives in the panel app's Vite config — there is no separate
114
250
  </Steps>
115
251
 
116
252
  <Info>
117
- **Matching paths replace, new paths append.** If your route's path matches a built-in page (e.g. `src/routes/products/page.tsx` → `/products`), your page **replaces** the built-in one — that's the override mechanism, no registration needed. Any other path is added alongside the built-in routes. Delete the file and the built-in page returns.
253
+ **Matching paths replace, new paths append.** If your route's path matches a built-in page (e.g. `src/routes/products/page.tsx` → `/products`), your page **replaces** the built-in one. Any other path is added alongside the built-in routes. Delete the file and the built-in page returns. To change *part* of a built-in page without owning it, prefer a widget or a custom field.
118
254
  </Info>
119
255
 
120
256
  ### Routing conventions
@@ -131,43 +267,6 @@ File paths map to URL routes automatically:
131
267
  | `src/routes/(settings)/page.tsx` | Route grouping | Groups routes without adding a URL segment |
132
268
  | `src/routes/dashboard/@sidebar/page.tsx` | Parallel route | Renders alongside parent |
133
269
 
134
- ## Navigation
135
-
136
- Sidebar items are generated from pages that export a `config` with a `label`.
137
-
138
- ### Ordering
139
-
140
- Use `rank` to control the order. Lower values appear higher:
141
-
142
- ```tsx
143
- // src/routes/orders/page.tsx
144
- export const config: RouteConfig = {
145
- label: "Orders",
146
- icon: ShoppingCart,
147
- rank: 1,
148
- }
149
-
150
- // src/routes/products/page.tsx
151
- export const config: RouteConfig = {
152
- label: "Products",
153
- icon: Tag,
154
- rank: 2,
155
- }
156
- ```
157
-
158
- ### Nested menus
159
-
160
- Use `nested` to group pages under a parent:
161
-
162
- ```tsx src/routes/settings/shipping/page.tsx
163
- export const config: RouteConfig = {
164
- label: "Shipping",
165
- nested: "/settings",
166
- }
167
- ```
168
-
169
- This places "Shipping" as a child item under `/settings` in the sidebar.
170
-
171
270
  ## Branding
172
271
 
173
272
  Set `name` and `logo` in the plugin options to customize the sidebar header:
@@ -180,54 +279,6 @@ mercurDashboardPlugin({
180
279
  })
181
280
  ```
182
281
 
183
- ## Replace components
184
-
185
- <Steps>
186
- <Step title="Write the replacement">
187
- Create the component anywhere under `src/`. It must have a default export:
188
-
189
- ```tsx src/components/custom-topbar-actions.tsx
190
- export default function CustomTopbarActions() {
191
- return (
192
- <div>
193
- <button>Help</button>
194
- <button>Notifications</button>
195
- </div>
196
- )
197
- }
198
- ```
199
- </Step>
200
- <Step title="Point the config at it">
201
- Register it in the `components` option. Paths are relative to `src/`:
202
-
203
- ```typescript vite.config.ts
204
- mercurDashboardPlugin({
205
- medusaConfigPath: '../../packages/api/medusa-config.ts',
206
- name: 'My Marketplace',
207
- components: {
208
- MainSidebar: 'components/custom-sidebar',
209
- SettingsSidebar: 'components/custom-settings-sidebar',
210
- TopbarActions: 'components/custom-topbar-actions',
211
- },
212
- })
213
- ```
214
- </Step>
215
- <Step title="Restart and verify">
216
- Restart the dev server — your component now renders everywhere its slot appears. Remove the entry and restart to get the built-in back; the originals are never modified.
217
- </Step>
218
- </Steps>
219
-
220
- | Component | Description |
221
- |-----------|-------------|
222
- | `MainSidebar` | Primary navigation sidebar |
223
- | `SettingsSidebar` | Settings section sidebar |
224
- | `TopbarActions` | Action buttons in the top bar |
225
- | `StoreSetup` | Store setup screen for new sellers (vendor portal) |
226
-
227
- <Warning>
228
- These four slots are the complete list — there is no generic "replace any component" registry. If what you want to change isn't one of them, it's a page concern: [re-compose the page](/rc/resources/tutorials/recompose-a-page) instead.
229
- </Warning>
230
-
231
282
  ## Internationalization
232
283
 
233
284
  <Steps>
@@ -274,36 +325,42 @@ mercurDashboardPlugin({
274
325
  ## FAQ
275
326
 
276
327
  <AccordionGroup>
277
- <Accordion title="Can I use Medusa's defineWidgetConfig or widget zones?">
278
- No. The Mercur panels don't load Medusa's admin extension system there are no widget zones to inject into. The equivalent workflows are: drop-in routes for new pages, [compound-component re-composition](/rc/resources/tutorials/recompose-a-page) for changing existing pages, and the four `components` overrides for global chrome. Backend customization (workflows, modules, subscribers) still follows standard Medusa conventions.
328
+ <Accordion title="Can I use Medusa's defineWidgetConfig and widget zones?">
329
+ Yes. `defineWidgetConfig` is re-exported from `@mercurjs/dashboard-sdk` and drives file-based widgets under `src/widgets/`. The difference from Medusa is the `before | after` placement suffix on the zone id and that each panel ships its own typed zone set. See [Add a widget](/rc/resources/tutorials/add-a-widget).
330
+ </Accordion>
331
+ <Accordion title="How do I add a field to a built-in form or detail page?">
332
+ Use `defineCustomFieldsConfig` in `src/custom-fields/<model>.tsx` — `forms[]` adds validated fields to create/edit forms, and `displays[]` adds/replaces/removes fields in detail sections. See [Extend forms and tables](/rc/resources/tutorials/extend-forms-and-tables).
279
333
  </Accordion>
280
334
  <Accordion title="How do I change just one part of a built-in page?">
281
- Import the page from the `/pages` subpath (`@mercurjs/vendor/pages` or `@mercurjs/admin/pages`), drop a route at the same path, and re-compose its slots keep the slots you don't change and they retain all their behavior (data fetching, filters, pagination). The [re-compose tutorial](/rc/resources/tutorials/recompose-a-page) walks through a real example.
335
+ For a spot inside the page (a banner, an extra field, a column), use a [widget](#widgets) or a [custom field](#custom-fields) — the rest of the page keeps all its behavior.
282
336
  </Accordion>
283
337
  <Accordion title="Why doesn't my page show up in the sidebar?">
284
338
  A sidebar item is only generated when the route file exports a `config` object with a `label`. Also check that the file is named exactly `page.tsx` (or `.ts`/`.jsx`/`.js`) under `src/routes/` and has a **default** export — files without one are skipped entirely.
285
339
  </Accordion>
340
+ <Accordion title="Why does my widget / custom field / nav override not type-check?">
341
+ Make sure the panel's typed targets are registered — a single `src/extension-targets.d.ts` with `/// <reference types="@mercurjs/vendor/extension-targets" />` (or the admin equivalent). Without it, zone/model/nav ids aren't known to TypeScript. `create-mercur-app` ships this file.
342
+ </Accordion>
286
343
  <Accordion title="Is there a mercur.config.ts file?">
287
344
  No — all panel configuration is passed inline to `mercurDashboardPlugin()` in `vite.config.ts`. If you've seen references to a separate config file, they're outdated.
288
345
  </Accordion>
289
346
  <Accordion title="Does this apply to both the admin panel and the vendor portal?">
290
- Yes — both panels use the same SDK and the same conventions. The differences are which built-in pages exist (`@mercurjs/admin/pages` vs `@mercurjs/vendor/pages`) and a few vendor-only options like `StoreSetup` and `enableSellerRegistration`.
347
+ Both panels use the same SDK and conventions, and a file only targets the panel it lives in. Which built-in zones, models, and nav ids exist differs per panel (each ships its own `extension-targets.d.ts`). Today the widget zones and product custom fields are mounted in the **vendor** portal; navigation overrides work in both.
291
348
  </Accordion>
292
349
  </AccordionGroup>
293
350
 
294
351
  ## Next steps
295
352
 
296
353
  <CardGroup cols={2}>
297
- <Card title="Add a custom panel page" href="/rc/resources/tutorials/custom-panel-page">
298
- Hands-on: your first drop-in route, end to end.
354
+ <Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
355
+ Inject a component at a built-in zone with `defineWidgetConfig`.
299
356
  </Card>
300
- <Card title="Re-compose a built-in page" href="/rc/resources/tutorials/recompose-a-page">
301
- Hands-on: override the vendor products page while keeping its table.
357
+ <Card title="Extend forms and tables" href="/rc/resources/tutorials/extend-forms-and-tables">
358
+ Add fields and columns with `defineCustomFieldsConfig`.
302
359
  </Card>
303
- <Card title="Replace panel components" href="/rc/resources/tutorials/replace-panel-components">
304
- Hands-on: swap the topbar actions and sidebar.
360
+ <Card title="Customize navigation" href="/rc/resources/tutorials/customize-navigation">
361
+ Reorder, hide, and re-parent sidebar items.
305
362
  </Card>
306
- <Card title="Build your own block" href="/rc/resources/tutorials/build-a-block">
307
- Package your customization and share it across projects.
363
+ <Card title="Add a custom panel page" href="/rc/resources/tutorials/custom-panel-page">
364
+ Your first drop-in route, end to end.
308
365
  </Card>
309
366
  </CardGroup>
@@ -0,0 +1,110 @@
1
+ ---
2
+ title: "Add a widget"
3
+ description: "Inject a React component at a named zone on a built-in panel page with defineWidgetConfig — no forking."
4
+ ---
5
+
6
+ A **widget** is a React component attached to a named zone on a built-in page. You drop one file under `src/widgets/`, and the SDK renders it at that zone while the rest of the page — its data fetching, filters, pagination — stays exactly as shipped. This is the lightest way to add UI to a page you don't own.
7
+
8
+ <Info>
9
+ **Additive, not a replacement.** Unlike a drop-in route (which owns the whole page), a widget layers your component onto the built-in page at a documented zone. Reach for this first when you just want to *add* something to an existing screen.
10
+ </Info>
11
+
12
+ ## What you'll build
13
+
14
+ A tip banner above the vendor product list, rendered from a single file — with the list itself untouched.
15
+
16
+ ## Register the typed targets (once)
17
+
18
+ Widget zones are typed ids that the vendor panel generates from its own pages and ships as `@mercurjs/vendor/extension-targets`. Register them once so the ids resolve everywhere, with a single ambient reference in your app's `src`:
19
+
20
+ ```typescript apps/vendor/src/extension-targets.d.ts
21
+ /// <reference types="@mercurjs/vendor/extension-targets" />
22
+ ```
23
+
24
+ Projects from `create-mercur-app` already ship this file. With it present, an invalid zone fails `tsc` instead of silently doing nothing.
25
+
26
+ ## Add the widget
27
+
28
+ <Steps>
29
+ <Step title="Create the widget file">
30
+ Drop a file under `src/widgets/`. Export the component as the **default** and a `config` built with `defineWidgetConfig`. The `zone` names where it renders:
31
+
32
+ ```tsx apps/vendor/src/widgets/product-list-banner.tsx
33
+ import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
34
+ import { Container, Text } from "@medusajs/ui"
35
+
36
+ export const config = defineWidgetConfig({
37
+ zone: "product.list.before",
38
+ })
39
+
40
+ export default function ProductListBanner() {
41
+ return (
42
+ <Container className="mb-2">
43
+ <Text size="small" className="text-ui-fg-subtle">
44
+ Tip: bulk-import products from the Products menu.
45
+ </Text>
46
+ </Container>
47
+ )
48
+ }
49
+ ```
50
+ </Step>
51
+ <Step title="Understand the zone id">
52
+ A zone id is `<domain>.<view>.<placement>`. The last segment is the placement:
53
+
54
+ | Placement | Effect |
55
+ |-----------|--------|
56
+ | `before` | Renders before the built-in content of the zone |
57
+ | `after` | Renders after the built-in content |
58
+
59
+ Multiple `before` / `after` widgets on the same zone stack in registration order.
60
+ </Step>
61
+ <Step title="Reload the panel">
62
+ Start the project (`bun run dev`) and open the vendor portal. Widget files hot-reload — the banner appears above the product list, and the table below it works exactly as before.
63
+ </Step>
64
+ </Steps>
65
+
66
+ ## Available zones
67
+
68
+ Zones mounted today in the **vendor portal**:
69
+
70
+ | Zone | Where it renders |
71
+ |------|------------------|
72
+ | `product.list.before` / `.after` | Vendor product list page |
73
+ | `login.logo.*` | The logo slot on the public login screen |
74
+ | `login.before.*` / `login.after.*` | Around the login form (rendered before authentication) |
75
+
76
+ The full, valid set is typed as `WidgetZoneId` and generated into `@mercurjs/vendor/extension-targets` from the panel's own zone hosts — let your editor autocomplete `zone:` to see every option. A zone no page renders can't be targeted and won't type-check.
77
+
78
+ ## Verify
79
+
80
+ 1. The tip banner renders above the product list.
81
+ 2. Search, filter, and paginate the list — all built-in behavior still works.
82
+ 3. Change the zone to `product.list.after` and reload — the banner moves below the list.
83
+ 4. Set `zone: "not.a.zone"` — `tsc` (`bun run lint`) fails with a "not assignable to `WidgetZoneId`" error.
84
+ 5. Delete the file — the banner disappears; nothing else changed.
85
+
86
+ ## FAQ
87
+
88
+ <AccordionGroup>
89
+ <Accordion title="Can a widget target more than one zone?">
90
+ Yes — `zone` accepts an array (`zone: ["product.list.before", "login.after.before"]`), and the same component renders at each.
91
+ </Accordion>
92
+ <Accordion title="Can a block ship widgets?">
93
+ Yes. A [block](/rc/learn/blocks) can include `src/widgets/` files in its `vendor_ui` / `admin_ui` entry, and they're aggregated just like the host app's — installing the block adds the widget with no wiring.
94
+ </Accordion>
95
+ <Accordion title="Does the admin panel have widget zones too?">
96
+ The zone set is per panel and generated from each panel's pages. Today the mounted zones live in the vendor portal (`product.list.*`, `login.*`); the admin panel exposes navigation and product custom fields. Check `@mercurjs/admin/extension-targets` for its current zones.
97
+ </Accordion>
98
+ </AccordionGroup>
99
+
100
+ ## Next steps
101
+
102
+ <CardGroup cols={2}>
103
+ <Card title="Extend forms and tables" href="/rc/resources/tutorials/extend-forms-and-tables">
104
+ Add validated fields and columns with defineCustomFieldsConfig.
105
+ </Card>
106
+ <Card title="Customize navigation" href="/rc/resources/tutorials/customize-navigation">
107
+ Reorder, hide, and re-parent sidebar items.
108
+ </Card>
109
+ </CardGroup>
110
+ </content>
@@ -6,7 +6,7 @@ description: "Add a page to the vendor portal with file-based routing — the da
6
6
  Adding a page to the admin panel or vendor portal takes one file. The dashboard SDK scans `src/routes/` at build time, registers the route, and — if you export a `config` — adds it to the sidebar with a label and icon. No route table, no registration call.
7
7
 
8
8
  <Info>
9
- **Adding vs changing.** This tutorial *adds* a brand-new page — the right move for new features. To *change* an existing page, don't rebuild it: drop a route at the same path and [re-compose the built-in page's slots](/rc/resources/tutorials/recompose-a-page). The [decision guide](/rc/resources/customization/extending-panels#choosing-your-extension-mechanism) compares every extension mechanism.
9
+ **Adding vs changing.** This tutorial *adds* a brand-new page — the right move for new features. To *change* an existing page, don't rebuild it: add a [widget](/rc/resources/tutorials/add-a-widget) or a [custom field](/rc/resources/tutorials/extend-forms-and-tables) to inject into it. The [decision guide](/rc/resources/customization/extending-panels#choosing-your-extension-mechanism) compares every extension mechanism.
10
10
  </Info>
11
11
 
12
12
  ## What you'll build
@@ -73,6 +73,6 @@ A `/reviews` page in the vendor portal with a sidebar entry — from a single fi
73
73
  ## Next steps
74
74
 
75
75
  <CardGroup cols={2}>
76
- <Card title="Re-compose a built-in page" href="/rc/resources/tutorials/recompose-a-page" />
76
+ <Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget" />
77
77
  <Card title="Add a custom API route" href="/rc/resources/tutorials/custom-api-route" />
78
78
  </CardGroup>
@@ -0,0 +1,111 @@
1
+ ---
2
+ title: "Customize navigation"
3
+ description: "Reorder, hide, relabel, and re-parent built-in sidebar items with a single _navigation.ts file and defineNavigationConfig."
4
+ ---
5
+
6
+ The built-in sidebar ships a fixed set of items — Orders, Products, Customers, and so on. To reshape them without replacing the whole sidebar, author one host-owned file: `src/_navigation.ts`. It reorders, hides, relabels, and re-parents **built-in** items, and it is the single source of truth for the sidebar's shape.
7
+
8
+ <Info>
9
+ **When to use this vs. a `config` export.** New pages you add via [drop-in routes](/rc/resources/tutorials/custom-panel-page) place their own sidebar item through `defineRouteConfig({ label, rank, nested })`. `_navigation.ts` is for the items you *didn't* create — the built-in ones. The two layer cleanly: custom routes place themselves; `_navigation.ts` reshapes the built-ins.
10
+ </Info>
11
+
12
+ ## What you'll build
13
+
14
+ A vendor sidebar with Orders pinned to the top, Price Lists hidden, and Campaigns moved under Orders.
15
+
16
+ ## Register the typed targets (once)
17
+
18
+ Nav item ids are typed and generated per panel. Register them once with a single ambient reference in your app's `src` (already shipped by `create-mercur-app`):
19
+
20
+ ```typescript apps/vendor/src/extension-targets.d.ts
21
+ /// <reference types="@mercurjs/vendor/extension-targets" />
22
+ ```
23
+
24
+ With it present, `id` and `nested` autocomplete and an unknown id fails `tsc`.
25
+
26
+ ## Author the navigation file
27
+
28
+ <Steps>
29
+ <Step title="Create src/_navigation.ts">
30
+ The file is host-owned and underscore-prefixed. Default-export a `defineNavigationConfig` with an `items` array of overrides:
31
+
32
+ ```ts apps/vendor/src/_navigation.ts
33
+ import { defineNavigationConfig } from "@mercurjs/dashboard-sdk"
34
+
35
+ export default defineNavigationConfig({
36
+ items: [
37
+ { id: "orders", rank: 0 }, // pin to the top
38
+ { id: "price-lists", hidden: true }, // hide from the sidebar
39
+ { id: "campaigns", nested: "orders" }, // re-parent under Orders
40
+ ],
41
+ })
42
+ ```
43
+ </Step>
44
+ <Step title="Know the override fields">
45
+ Each entry targets one built-in item by its stable `id`:
46
+
47
+ | Field | Type | Effect |
48
+ |-------|------|--------|
49
+ | `id` | `NavItemId` | **Required.** The built-in item to override (top-level or nested) |
50
+ | `rank` | `number` | Order within its parent (lower first) |
51
+ | `hidden` | `boolean` | Remove it from the sidebar |
52
+ | `label` | `string` | Relabel (i18n key or literal) |
53
+ | `icon` | `ComponentType` | Replace its icon |
54
+ | `nested` | `NavParentId \| null` | Re-parent under another top-level item; `null` promotes a nested item to top level |
55
+
56
+ Both `id` and `nested` are checked against the panel's generated `NavItemRegistry` / `NavParentRegistry`.
57
+ </Step>
58
+ <Step title="Reload the panel">
59
+ Open the vendor portal — Orders sits at the top, Price Lists is gone from the menu, and Campaigns now appears under Orders. The route for a hidden item stays reachable directly by URL unless you also remove it.
60
+ </Step>
61
+ </Steps>
62
+
63
+ ## Common recipes
64
+
65
+ ```ts
66
+ export default defineNavigationConfig({
67
+ items: [
68
+ { id: "payouts", label: "Settlements" }, // relabel
69
+ { id: "categories", nested: null, rank: 1 }, // promote a nested item to top level
70
+ { id: "collections", nested: "orders" }, // move a nested item under a different parent
71
+ { id: "inventory", hidden: true }, // hide a built-in
72
+ ],
73
+ })
74
+ ```
75
+
76
+ ## Verify
77
+
78
+ 1. The top-level order reflects your `rank` values, with `orders` first.
79
+ 2. `price-lists` no longer appears in the sidebar.
80
+ 3. `campaigns` renders as a child under Orders.
81
+ 4. Set `id: "not-an-item"` — `bun run lint` (tsc) fails against `NavItemRegistry`.
82
+ 5. Delete `_navigation.ts` — the default sidebar returns.
83
+
84
+ ## FAQ
85
+
86
+ <AccordionGroup>
87
+ <Accordion title="Can an installed block reorder the sidebar?">
88
+ No. Navigation is deliberately host-only — blocks can ship pages, widgets, and custom fields, but the sidebar order stays a single source of truth in your app's `_navigation.ts`.
89
+ </Accordion>
90
+ <Accordion title="What ids can I target?">
91
+ Any built-in item, top-level or nested, by its own id — e.g. `orders`, `products`, `categories`, `collections`, `campaigns`, `customer-groups`. Let your editor autocomplete `id:` against `NavItemId`; the full set is generated into your panel's `extension-targets.d.ts`.
92
+ </Accordion>
93
+ <Accordion title="Does this work in the admin panel too?">
94
+ Yes — drop `src/_navigation.ts` in the admin app and reference `@mercurjs/admin/extension-targets`. Each panel ships its own nav id set.
95
+ </Accordion>
96
+ <Accordion title="How do I add a brand-new sidebar item?">
97
+ That's a [drop-in route](/rc/resources/tutorials/custom-panel-page) with a `config` export — `_navigation.ts` only reshapes built-in items, it doesn't create routes.
98
+ </Accordion>
99
+ </AccordionGroup>
100
+
101
+ ## Next steps
102
+
103
+ <CardGroup cols={2}>
104
+ <Card title="Add a custom panel page" href="/rc/resources/tutorials/custom-panel-page">
105
+ Add a new screen with its own sidebar item.
106
+ </Card>
107
+ <Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
108
+ Inject a component into a built-in page.
109
+ </Card>
110
+ </CardGroup>
111
+ </content>
@@ -0,0 +1,189 @@
1
+ ---
2
+ title: "Extend forms and tables"
3
+ description: "Add validated fields to built-in forms, replace fields in detail sections, and add list columns with defineCustomFieldsConfig and createFormHelper."
4
+ ---
5
+
6
+ `defineCustomFieldsConfig` is Mercur's model-scoped extension surface: from one file per model, you add validated fields to built-in create/edit forms, replace/remove/add fields in detail sections, and add columns to the list table — all wired into the built-in page, all typed against the model's generated registry.
7
+
8
+ <Info>
9
+ **UI, not schema.** This helper is a *panel* surface — it renders, validates, and displays fields. It does not create database columns. To *store* extra data, use the backend [Custom Fields module](/rc/resources/customization/custom-fields) or your own API route/workflow. In the MVP, panel custom fields for `product` are submitted under `additional_data` and persisted onto the product's `metadata`.
10
+ </Info>
11
+
12
+ ## What you'll build
13
+
14
+ An `ERP ID` field on the vendor product edit form, shown in the product's detail section and as a list-table column — from a single `src/custom-fields/product.tsx`.
15
+
16
+ ## Register the typed targets (once)
17
+
18
+ Models, form zones, display zones, and built-in field ids are typed per panel. Register them once (shipped by `create-mercur-app`):
19
+
20
+ ```typescript apps/vendor/src/extension-targets.d.ts
21
+ /// <reference types="@mercurjs/vendor/extension-targets" />
22
+ ```
23
+
24
+ ## Build the config
25
+
26
+ <Steps>
27
+ <Step title="Create the model file">
28
+ Drop `src/custom-fields/<model>.tsx` and default-export a `defineCustomFieldsConfig`. `createFormHelper` (from `@mercurjs/dashboard-shared`) turns a Zod schema into an input type + validation:
29
+
30
+ ```tsx apps/vendor/src/custom-fields/product.tsx
31
+ import { defineCustomFieldsConfig } from "@mercurjs/dashboard-sdk"
32
+ import { createFormHelper } from "@mercurjs/dashboard-shared"
33
+
34
+ type ProductWithMeta = { metadata?: Record<string, unknown> }
35
+ const form = createFormHelper<ProductWithMeta>()
36
+
37
+ export default defineCustomFieldsConfig({
38
+ model: "product",
39
+ forms: [
40
+ {
41
+ zone: "edit",
42
+ fields: {
43
+ erp_id: form.define({
44
+ validation: form.string().optional(),
45
+ label: "ERP ID",
46
+ description: "External system identifier",
47
+ placeholder: "ERP-000",
48
+ defaultValue: (data) => (data?.metadata?.erp_id as string) ?? "",
49
+ }),
50
+ },
51
+ },
52
+ ],
53
+ })
54
+ ```
55
+ </Step>
56
+ <Step title="Add a detail-section display">
57
+ `displays[]` targets a detail-page section by its `zone` id. Keyed by field `id`, an entry **adds**, **replaces**, or **removes** a field:
58
+
59
+ ```tsx
60
+ import { Text } from "@medusajs/ui"
61
+
62
+ // ...inside defineCustomFieldsConfig:
63
+ displays: [
64
+ {
65
+ zone: "general",
66
+ fields: [
67
+ // ADD — an unknown id appends a new read-only row
68
+ {
69
+ id: "erp_id",
70
+ component: ({ data }) => (
71
+ <Text size="small" className="text-ui-fg-subtle px-6 py-4">
72
+ ERP ID: {String(data?.metadata?.erp_id ?? "-")}
73
+ </Text>
74
+ ),
75
+ },
76
+ // REMOVE — a built-in id + null hides the field
77
+ { id: "subtitle", component: null },
78
+ // REPLACE — a built-in id + component overrides its render
79
+ {
80
+ id: "handle",
81
+ component: ({ data }) => (
82
+ <Text size="small" className="text-ui-fg-subtle px-6 py-4">
83
+ /{(data as { handle?: string })?.handle}
84
+ </Text>
85
+ ),
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ ```
91
+
92
+ Built-in field ids (like `subtitle`, `handle`, `status`, `title`) autocomplete from the panel's generated `CustomFieldsRegistry`; an unknown id is treated as an added row.
93
+ </Step>
94
+ <Step title="Add a list column">
95
+ The `list` block extends the model's list table — add or override columns by id, hide built-in columns, and reorder:
96
+
97
+ ```tsx
98
+ // ...inside defineCustomFieldsConfig:
99
+ list: {
100
+ columns: [
101
+ {
102
+ id: "erp_id",
103
+ header: "ERP",
104
+ component: ({ row }) => String(row.metadata?.erp_id ?? "-"),
105
+ },
106
+ ],
107
+ viewDefaults: {
108
+ columnVisibility: { collection: false }, // hide a built-in column
109
+ columnOrder: ["product", "erp_id", "status"], // reorder
110
+ },
111
+ },
112
+ ```
113
+ </Step>
114
+ <Step title="Reload the panel">
115
+ Open the vendor portal. The product **edit** drawer shows the ERP ID field (validated on submit and persisted via `additional_data`), the detail **general** section shows the ERP ID row (with `subtitle` removed and `handle` re-rendered), and the product **list** shows the ERP column.
116
+ </Step>
117
+ </Steps>
118
+
119
+ ## The `createFormHelper` surface
120
+
121
+ `createFormHelper<T>()` exposes a Zod-based surface that drives both the input type and its validation:
122
+
123
+ ```ts
124
+ const form = createFormHelper<T>()
125
+
126
+ form.define({
127
+ validation: form.string().min(1), // string | number | boolean | date | array | object | null | nullable | coerce
128
+ label: "…",
129
+ description: "…",
130
+ placeholder: "…",
131
+ defaultValue: "" | ((data) => /* derive from the entity */),
132
+ component, // optional custom render component
133
+ })
134
+ ```
135
+
136
+ Fields render through the standard `Form.Field → Form.Item` chain (never a raw `Controller`) and participate in the existing `TabbedForm` / `RouteDrawer` submit and validation flow.
137
+
138
+ ## Linked-module data
139
+
140
+ To read data from a linked module alongside the entity, declare it with `link`; those relations are fetched with the entity and become available to columns and displays:
141
+
142
+ ```ts
143
+ export default defineCustomFieldsConfig({
144
+ model: "product",
145
+ link: "brand", // or ["brand", "warranty"]
146
+ list: {
147
+ columns: [{ id: "brand_name", header: "Brand", component: ({ row }) => row.brand?.name }],
148
+ },
149
+ })
150
+ ```
151
+
152
+ The SDK derives the fetch query from `link` and merges it into the built-in query with the `+`/`-` convention — you never hand-write the field list.
153
+
154
+ ## Verify
155
+
156
+ 1. The ERP ID field renders in the product edit drawer and validates on submit.
157
+ 2. Saving persists the value (visible on reload) via `additional_data` → `metadata`.
158
+ 3. The detail general section shows the ERP row, hides `subtitle`, and re-renders `handle`.
159
+ 4. The product list shows the ERP column, hides `collection`, and reorders columns.
160
+ 5. Set `zone: "nope"` in `forms` — `bun run lint` (tsc) fails against the model's registry.
161
+
162
+ ## FAQ
163
+
164
+ <AccordionGroup>
165
+ <Accordion title="Which models and zones are available?">
166
+ Today: the `product` model in the vendor portal, with form zone `edit` and display zone `general`. The valid set per panel is generated into `CustomFieldsRegistry` in `extension-targets.d.ts` — autocomplete `model` / `zone` to see what's mounted.
167
+ </Accordion>
168
+ <Accordion title="Where is the value actually stored?">
169
+ In the MVP, product custom fields are submitted under `additional_data` and persisted onto `product.metadata`. `defineCustomFieldsConfig` itself doesn't create a column — for durable, queryable storage model it with the backend [Custom Fields module](/rc/resources/customization/custom-fields) or a custom route/workflow.
170
+ </Accordion>
171
+ <Accordion title="Can I extend the onboarding wizard?">
172
+ That's the same helper with `zone: "onboarding"` and `tab` set to a wizard step id (vendor only). It's designed but not mounted in the current MVP — the runtime host exists; the wizard mount is a follow-up.
173
+ </Accordion>
174
+ <Accordion title="Can a block ship custom fields?">
175
+ Yes — a [block](/rc/learn/blocks) can include `src/custom-fields/` files in its `vendor_ui` / `admin_ui` entry, aggregated like the host app's.
176
+ </Accordion>
177
+ </AccordionGroup>
178
+
179
+ ## Next steps
180
+
181
+ <CardGroup cols={2}>
182
+ <Card title="Custom Fields module" href="/rc/resources/customization/custom-fields">
183
+ Persist extra data on an entity with a generated side table.
184
+ </Card>
185
+ <Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
186
+ Inject a component at a built-in zone.
187
+ </Card>
188
+ </CardGroup>
189
+ </content>
@@ -6,7 +6,7 @@ description: "Install the product-import-export block and give vendors CSV impor
6
6
  Bulk catalog operations belong to vendors, not the operator — a seller migrating from another platform needs to bring hundreds of listings with them. The `product-import-export` block adds CSV import and export to the Vendor Portal: API routes, workflows built on Medusa's product CSV steps, and drawer UIs wired into the product list.
7
7
 
8
8
  <Info>
9
- **This block is also a masterclass in Mercur's extension model.** It ships backend workflows and routes into your API package, and it overrides the vendor `/products` page by dropping a route file that [re-composes the built-in page's compound slots](/rc/resources/tutorials/recompose-a-page) — adding Import/Export buttons while keeping the stock table untouched. After installing, read its source; you own every file.
9
+ **This block is also a masterclass in Mercur's extension model.** It ships backend workflows and routes into your API package, and it adds Import/Export actions to the vendor products area through the panel extension surface. After installing, read its source; you own every file.
10
10
  </Info>
11
11
 
12
12
  ## What you'll build
@@ -87,8 +87,8 @@ A vendor product list with **Import** and **Export** header buttons, a working C
87
87
  ## Next steps
88
88
 
89
89
  <CardGroup cols={2}>
90
- <Card title="Re-compose a built-in page" href="/rc/resources/tutorials/recompose-a-page">
91
- The page-override technique this block uses, explained step by step.
90
+ <Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
91
+ Inject a component at a built-in zone with `defineWidgetConfig`.
92
92
  </Card>
93
93
  <Card title="Build your own block" href="/rc/resources/tutorials/build-a-block">
94
94
  Package a feature like this one and publish it to your own registry.
package/llms.txt CHANGED
@@ -30,23 +30,24 @@ package (`node_modules/@mercurjs/docs/`).
30
30
  - [Building with AI](content/resources/ai/overview.mdx) — Mercur ships version-matched docs inside your project so AI coding agents build from accurate APIs instead of stale training data.
31
31
  - [Custom Fields](content/resources/customization/custom-fields.mdx) — Extend any Medusa entity with additional fields without modifying core code.
32
32
  - [Extend a workflow](content/resources/customization/extend-a-workflow.mdx) — Inject custom logic into an existing Mercur workflow through hooks — without rewriting it.
33
- - [Extending Panels](content/resources/customization/extending-panels.mdx) — Add pages, customize navigation, branding, and replace components in the admin and vendor panels.
33
+ - [Extending Panels](content/resources/customization/extending-panels.mdx) — Add pages, inject widgets, customize navigation, and extend forms and tables in the admin and vendor panels.
34
34
  - [Medusa Cloud deployment](content/resources/deployment/medusa-cloud.mdx) — Deploy Mercur — backend, admin panel, and vendor panel — on Medusa Cloud.
35
35
  - [Notifications](content/resources/integrations/notifications.mdx) — Send transactional emails and marketplace notifications with Resend.
36
36
  - [Search](content/resources/integrations/search.mdx) — Mercur's provider-agnostic search module — zero-infrastructure Orama by default, swappable for Algolia, Meilisearch, or your own provider.
37
37
  - [Stripe Connect Integration](content/resources/integrations/stripe-connect.mdx) — Set up Stripe Connect for marketplace payments and seller payouts — from Stripe Dashboard configuration to the full end-to-end payment lifecycle.
38
38
  - [Add a feature with a block](content/resources/tutorials/add-a-block.mdx) — Install the reviews block end-to-end and see it live across the admin, vendor, and storefront surfaces.
39
+ - [Add a widget](content/resources/tutorials/add-a-widget.mdx) — Inject a React component at a named zone on a built-in panel page with defineWidgetConfig — no forking.
39
40
  - [Create attributes and variant axes](content/resources/tutorials/attributes-and-variant-axes.mdx) — Build the attribute catalog: a filterable attribute, a variant axis backed by a native product option, and an inline product-scoped axis.
40
41
  - [Build your own block](content/resources/tutorials/build-a-block.mdx) — Author a reusable feature as a block — backend, panel UI, and docs — build it into a registry, and install it into any Mercur project.
41
42
  - [Configure commissions](content/resources/tutorials/configure-commissions.mdx) — Set the global commission, add scoped rules for categories and sellers, and confirm the right rate lands on an order.
42
43
  - [Add a custom API route](content/resources/tutorials/custom-api-route.mdx) — Create a backend endpoint, regenerate the route map, and call it from a panel page with full type safety end to end.
43
44
  - [Add a custom panel page](content/resources/tutorials/custom-panel-page.mdx) — Add a page to the vendor portal with file-based routing — the dashboard SDK wires it in automatically.
45
+ - [Customize navigation](content/resources/tutorials/customize-navigation.mdx) — Reorder, hide, relabel, and re-parent built-in sidebar items with a single _navigation.ts file and defineNavigationConfig.
46
+ - [Extend forms and tables](content/resources/tutorials/extend-forms-and-tables.mdx) — Add validated fields to built-in forms, replace fields in detail sections, and add list columns with defineCustomFieldsConfig and createFormHelper.
44
47
  - [Build your first marketplace](content/resources/tutorials/first-marketplace.mdx) — Go end-to-end: create a project, approve a seller, list a product, place a split order, and watch a payout settle.
45
48
  - [Handle product requests](content/resources/tutorials/handle-product-requests.mdx) — Walk a vendor's product submission and a follow-up edit through the operator review pipeline — confirm, decline, or send back.
46
49
  - [Import and export products via CSV](content/resources/tutorials/import-export-products.mdx) — Install the product-import-export block and give vendors CSV import and export drawers on their product list.
47
50
  - [Work with master products and offers](content/resources/tutorials/master-products-and-offers.mdx) — Two sellers compete on one catalog entry: publish a master product, create competing offers, and read per-offer prices from the Store API.
48
- - [Re-compose a built-in page](content/resources/tutorials/recompose-a-page.mdx) — Override a built-in panel page by dropping a route file at the same path and re-composing its compound component slots.
49
- - [Replace panel components](content/resources/tutorials/replace-panel-components.mdx) — Swap the sidebar, topbar actions, or store setup screen for your own components through dashboard SDK configuration.
50
51
  - [Set up seller payouts](content/resources/tutorials/seller-payouts-stripe.mdx) — Take a seller from zero to paid: Stripe Connect onboarding, an order through fulfillment, capture, and the automated payout.
51
52
 
52
53
  ## Tools — CLI, API client, dashboard SDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercurjs/docs",
3
- "version": "2.2.0-canary.40",
3
+ "version": "2.2.0-canary.41",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/mercurjs/mercur",
@@ -1,129 +0,0 @@
1
- ---
2
- title: "Re-compose a built-in page"
3
- description: "Override a built-in panel page by dropping a route file at the same path and re-composing its compound component slots."
4
- ---
5
-
6
- Every page in the admin and vendor panels is exported as a **compound component** — a root plus named slots like `Header`, `HeaderActions`, and `DataTable`. That means you never fork a page to change one part of it: you drop a `page.tsx` at the same route path, render the original page, and swap only the slot you care about. Everything you don't touch keeps its default behavior — data fetching, filters, pagination, i18n, all of it.
7
-
8
- <Info>
9
- **This is not Medusa's widget system.** Medusa's admin lets you inject widgets into predefined zones around a page. Mercur takes a different approach: pages are React compound components you re-compose directly. There are no injection zones and no `defineWidgetConfig` — you get the actual page component and decide what renders inside it. See [Extending Panels](/rc/resources/customization/extending-panels) for the full comparison.
10
- </Info>
11
-
12
- ## What you'll build
13
-
14
- A vendor portal `/products` page with **Import** and **Export** buttons added next to the built-in Create button — while the table, search, filters, and everything else stay exactly as shipped. This is the same technique the official `product-import-export` block uses.
15
-
16
- ## Slot anatomy
17
-
18
- Built-in pages are exported from the `/pages` subpath of each panel package:
19
-
20
- ```typescript
21
- import { ProductListPage } from "@mercurjs/vendor/pages" // vendor portal
22
- import { CustomerListPage } from "@mercurjs/admin/pages" // admin panel
23
- ```
24
-
25
- Each list page exposes the same family of slots:
26
-
27
- | Slot | Renders |
28
- |------|---------|
29
- | `Table` | The `Container` shell holding header + table |
30
- | `Header` | The title/actions row |
31
- | `HeaderTitle` | Heading and subtitle |
32
- | `HeaderActions` | The action button cluster |
33
- | `HeaderCreateButton` | The built-in Create button |
34
- | `DataTable` | The wired data table — fetching, columns, filters, pagination |
35
-
36
- Detail pages expose section slots instead (e.g. `CustomerDetailPage.Main`, `CustomerDetailPage.Sidebar`, `CustomerDetailPage.MainGeneralSection`).
37
-
38
- ## Override the page
39
-
40
- <Steps>
41
- <Step title="Drop a route file at the same path">
42
- Create the file in your vendor app at the path that matches the built-in route. `/products` maps to `src/routes/products/page.tsx`:
43
-
44
- ```tsx apps/vendor/src/routes/products/page.tsx
45
- import { Link } from "react-router-dom"
46
- import { Button } from "@medusajs/ui"
47
- import { ArrowDownTray, ArrowUpTray } from "@medusajs/icons"
48
- import { ProductListPage } from "@mercurjs/vendor/pages"
49
-
50
- export default function ProductsWithImportExport() {
51
- return (
52
- <ProductListPage>
53
- <ProductListPage.Table>
54
- <ProductListPage.Header>
55
- <ProductListPage.HeaderTitle />
56
- <ProductListPage.HeaderActions>
57
- <Button size="small" variant="secondary" asChild>
58
- <Link to="import">
59
- <ArrowUpTray />
60
- Import
61
- </Link>
62
- </Button>
63
- <Button size="small" variant="secondary" asChild>
64
- <Link to="export">
65
- <ArrowDownTray />
66
- Export
67
- </Link>
68
- </Button>
69
- <ProductListPage.HeaderCreateButton />
70
- </ProductListPage.HeaderActions>
71
- </ProductListPage.Header>
72
- <ProductListPage.DataTable />
73
- </ProductListPage.Table>
74
- </ProductListPage>
75
- )
76
- }
77
- ```
78
- </Step>
79
- <Step title="Reuse the slots you don't change">
80
- Note what happened in that file: `HeaderTitle`, `HeaderCreateButton`, and `DataTable` are the originals, rendered untouched. Only `HeaderActions` gained two buttons. You re-compose top-down — render the page, re-declare only the branch you're changing, and keep original slots for everything inside it.
81
- </Step>
82
- <Step title="Reload the panel">
83
- The dashboard SDK picks the file up automatically (adding or removing a route file triggers a full reload in dev). Because the path matches a built-in route, **your page replaces the built-in one** — no configuration, no registration.
84
- </Step>
85
- </Steps>
86
-
87
- <Note>
88
- When a custom route's path matches a built-in route, your page replaces the built-in one. Routes at new paths are appended instead. That one rule is the entire override mechanism.
89
- </Note>
90
-
91
- ## How defaults work
92
-
93
- If you render a compound page with **no children**, it renders its full default composition — so `<ProductListPage />` alone is identical to the built-in page. Every page root follows the pattern:
94
-
95
- ```tsx
96
- Children.count(children) > 0 ? children : <DefaultComposition />
97
- ```
98
-
99
- ## Verify
100
-
101
- 1. Run your project (`bun run dev`) and open the vendor portal.
102
- 2. Navigate to **Products** — the list should look identical to before, plus Import and Export buttons in the header.
103
- 3. Search, filter, and paginate the table — all built-in behavior must still work, since `DataTable` is untouched.
104
- 4. Delete your `src/routes/products/page.tsx` and reload — the built-in page comes back. Nothing was forked.
105
-
106
- ## FAQ
107
-
108
- <AccordionGroup>
109
- <Accordion title="Where do I find which slots a page exposes?">
110
- Import the page from `@mercurjs/vendor/pages` or `@mercurjs/admin/pages` and let your editor's autocomplete list the attached members — every slot is a static property on the page component. The naming is consistent across pages: list pages expose `Table`/`Header`/`HeaderTitle`/`HeaderActions`/`HeaderCreateButton`/`DataTable`; detail pages expose `Main`/`Sidebar` plus one property per section.
111
- </Accordion>
112
- <Accordion title="Can I remove a built-in element, like the Create button?">
113
- Yes — re-composition is declarative. Anything you don't render doesn't appear: re-declare `HeaderActions` with only your own buttons and omit `HeaderCreateButton`.
114
- </Accordion>
115
- <Accordion title="What about changing the table columns?">
116
- `DataTable` is a single slot — you either keep it wholesale or replace it with your own table. For a different column set, replace the slot with your own component built on the shared `DataTable`/`useDataTable` primitives from the panel package.
117
- </Accordion>
118
- </AccordionGroup>
119
-
120
- ## Next steps
121
-
122
- <CardGroup cols={2}>
123
- <Card title="Replace panel components" href="/rc/resources/tutorials/replace-panel-components">
124
- Swap global chrome like the sidebar or topbar instead of a single page.
125
- </Card>
126
- <Card title="Add a custom API route" href="/rc/resources/tutorials/custom-api-route">
127
- Back your custom page with a typed endpoint of your own.
128
- </Card>
129
- </CardGroup>
@@ -1,116 +0,0 @@
1
- ---
2
- title: "Replace panel components"
3
- description: "Swap the sidebar, topbar actions, or store setup screen for your own components through dashboard SDK configuration."
4
- ---
5
-
6
- Some customizations aren't about one page — they change the panel's global chrome. For those, the dashboard SDK exposes a small set of **named component overrides**: you point the plugin config at your own file, and the panel renders your component in place of the built-in one everywhere it appears.
7
-
8
- <Info>
9
- **Choose the right tool.** A component override replaces one of four global layout slots across the whole panel. If you want to change a single page, [re-compose it](/rc/resources/tutorials/recompose-a-page) instead — and if you want to add a new screen, just [drop in a route](/rc/resources/tutorials/custom-panel-page). The decision guide in [Extending Panels](/rc/resources/customization/extending-panels#choosing-your-extension-mechanism) compares all three.
10
- </Info>
11
-
12
- ## What you'll build
13
-
14
- A vendor portal with custom topbar actions (a help link and a status badge) and a custom main sidebar, configured entirely from `vite.config.ts`.
15
-
16
- ## The available slots
17
-
18
- Exactly four layout components can be replaced:
19
-
20
- | Component | Where it renders |
21
- |-----------|------------------|
22
- | `MainSidebar` | Primary navigation sidebar on every main page |
23
- | `SettingsSidebar` | Sidebar of the `/settings` section |
24
- | `TopbarActions` | Action cluster on the right side of the top bar |
25
- | `StoreSetup` | The store setup screen shown to new sellers (vendor portal) |
26
-
27
- Anything else — a page, a section, a table — is customized through routes and compound components, not through overrides.
28
-
29
- ## Set up the override
30
-
31
- <Steps>
32
- <Step title="Write the replacement component">
33
- Create the component anywhere under `src/`. It must have a **default export**:
34
-
35
- ```tsx apps/vendor/src/components/topbar-actions.tsx
36
- import { Badge, Button } from "@medusajs/ui"
37
-
38
- export default function TopbarActions() {
39
- return (
40
- <div className="flex items-center gap-x-2">
41
- <Badge size="2xsmall" color="green">
42
- All systems operational
43
- </Badge>
44
- <Button size="small" variant="transparent" asChild>
45
- <a href="https://help.example.com" target="_blank" rel="noreferrer">
46
- Help
47
- </a>
48
- </Button>
49
- </div>
50
- )
51
- }
52
- ```
53
- </Step>
54
- <Step title="Register it in the Vite config">
55
- Component overrides live in the `components` option of `mercurDashboardPlugin`, in your panel app's `vite.config.ts`. Paths are **relative to `src/`**:
56
-
57
- ```typescript apps/vendor/vite.config.ts
58
- import { defineConfig } from 'vite'
59
- import react from '@vitejs/plugin-react'
60
- import { mercurDashboardPlugin } from '@mercurjs/dashboard-sdk'
61
-
62
- export default defineConfig({
63
- plugins: [
64
- react(),
65
- mercurDashboardPlugin({
66
- medusaConfigPath: '../../packages/api/medusa-config.ts',
67
- components: {
68
- TopbarActions: 'components/topbar-actions',
69
- MainSidebar: 'components/main-sidebar',
70
- },
71
- }),
72
- ],
73
- })
74
- ```
75
-
76
- The plugin resolves each path at build time and swaps your component in through the `virtual:mercur/components` module — no runtime registration, no context providers to wire.
77
- </Step>
78
- <Step title="Restart the dev server">
79
- Vite config changes require a restart. After it, your components render everywhere their slots appear.
80
- </Step>
81
- </Steps>
82
-
83
- <Tip>
84
- Build overrides with `@medusajs/ui` components and Medusa UI color tokens so they match the rest of the panel. The panels ship no other UI library.
85
- </Tip>
86
-
87
- ## Verify
88
-
89
- 1. Open the vendor portal — the top bar shows your badge and Help link on every page.
90
- 2. Navigate between main pages and `/settings` — the override applies everywhere its slot renders.
91
- 3. Remove the `components` entry and restart — the built-in components return. The originals were never modified.
92
-
93
- ## FAQ
94
-
95
- <AccordionGroup>
96
- <Accordion title="If I replace MainSidebar, do my custom routes still show up in the menu?">
97
- Only if your sidebar renders them. A `MainSidebar` override replaces the **entire** navigation sidebar, including the auto-generated menu items from route `config` exports — read them from `virtual:mercur/menu-items` if you want to keep automatic navigation. If you only want to add or reorder items, you don't need an override at all: the `config` export on route files (with `rank` and `nested`) covers that.
98
- </Accordion>
99
- <Accordion title="Can I override other components, like the login page or a form?">
100
- No — these four slots are the complete list. The login flow, forms, and everything page-level are customized through drop-in routes and [compound re-composition](/rc/resources/tutorials/recompose-a-page).
101
- </Accordion>
102
- <Accordion title="Do overrides apply to both panels?">
103
- Each panel app has its own `vite.config.ts`, so overrides are configured per panel. `StoreSetup` only exists in the vendor portal; the other three slots exist in both.
104
- </Accordion>
105
- </AccordionGroup>
106
-
107
- ## Next steps
108
-
109
- <CardGroup cols={2}>
110
- <Card title="Re-compose a built-in page" href="/rc/resources/tutorials/recompose-a-page">
111
- Change one page's composition instead of global chrome.
112
- </Card>
113
- <Card title="Extending Panels" href="/rc/resources/customization/extending-panels">
114
- The full reference for routes, navigation, branding, and overrides.
115
- </Card>
116
- </CardGroup>