@mercurjs/docs 2.2.0-rc.0 → 2.2.0-rc.2

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.
Files changed (41) hide show
  1. package/content/learn/concepts.mdx +1 -1
  2. package/content/references/overview.mdx +10 -0
  3. package/content/references/panel-extension-api.mdx +337 -0
  4. package/content/references/workflows/cart/add-seller-shipping-method-to-cart.mdx +5 -1
  5. package/content/references/workflows/commission/batch-commission-rules.mdx +6 -2
  6. package/content/references/workflows/offer/batch-offer-inventory-items.mdx +6 -2
  7. package/content/references/workflows/offer/create-offers.mdx +3 -1
  8. package/content/references/workflows/offer/update-offers.mdx +3 -1
  9. package/content/references/workflows/order-group/get-order-groups-list.mdx +3 -1
  10. package/content/references/workflows/payout/process-payout-for-webhook.mdx +3 -1
  11. package/content/references/workflows/product/confirm-products.mdx +2 -0
  12. package/content/references/workflows/product/reject-product.mdx +2 -0
  13. package/content/references/workflows/product/request-product-change.mdx +2 -0
  14. package/content/references/workflows/seller/approve-seller.mdx +2 -0
  15. package/content/references/workflows/seller/suspend-seller.mdx +2 -0
  16. package/content/references/workflows/seller/terminate-seller.mdx +2 -0
  17. package/content/references/workflows/seller/unsuspend-seller.mdx +2 -0
  18. package/content/references/workflows/seller/unterminate-seller.mdx +2 -0
  19. package/content/resources/best-practices/api-routes.mdx +306 -0
  20. package/content/resources/best-practices/custom-fields.mdx +241 -0
  21. package/content/resources/best-practices/frontend.mdx +286 -0
  22. package/content/resources/best-practices/module-links.mdx +157 -0
  23. package/content/resources/best-practices/modules.mdx +144 -0
  24. package/content/resources/best-practices/overview.mdx +115 -0
  25. package/content/resources/best-practices/subscribers-and-jobs.mdx +133 -0
  26. package/content/resources/best-practices/types.mdx +120 -0
  27. package/content/resources/best-practices/workflows.mdx +173 -0
  28. package/content/resources/customization/custom-fields.mdx +5 -1
  29. package/content/resources/customization/extending-panels.mdx +170 -113
  30. package/content/resources/tutorials/add-a-widget.mdx +110 -0
  31. package/content/resources/tutorials/add-order-detail-button.mdx +113 -0
  32. package/content/resources/tutorials/custom-panel-page.mdx +2 -2
  33. package/content/resources/tutorials/customize-navigation.mdx +110 -0
  34. package/content/resources/tutorials/extend-forms-and-tables.mdx +188 -0
  35. package/content/resources/tutorials/extend-onboarding.mdx +253 -0
  36. package/content/resources/tutorials/import-export-products.mdx +3 -3
  37. package/content/resources/tutorials/store-setup-checklist.mdx +214 -0
  38. package/llms.txt +17 -3
  39. package/package.json +1 -1
  40. package/content/resources/tutorials/recompose-a-page.mdx +0 -129
  41. package/content/resources/tutorials/replace-panel-components.mdx +0 -116
@@ -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
+ | `seller.setup.before` / `.after` | The store-setup / onboarding surface (dashboard home + store settings), passed the `seller` as `data` |
74
+ | `login.logo.*` | The logo slot on the public login screen |
75
+ | `login.before.*` / `login.after.*` | Around the login form (rendered before authentication) |
76
+
77
+ 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.
78
+
79
+ ## Verify
80
+
81
+ 1. The tip banner renders above the product list.
82
+ 2. Search, filter, and paginate the list — all built-in behavior still works.
83
+ 3. Change the zone to `product.list.after` and reload — the banner moves below the list.
84
+ 4. Set `zone: "not.a.zone"` — `tsc` (`bun run lint`) fails with a "not assignable to `WidgetZoneId`" error.
85
+ 5. Delete the file — the banner disappears; nothing else changed.
86
+
87
+ ## FAQ
88
+
89
+ <AccordionGroup>
90
+ <Accordion title="Can a widget target more than one zone?">
91
+ Yes — `zone` accepts an array (`zone: ["product.list.before", "login.after.before"]`), and the same component renders at each.
92
+ </Accordion>
93
+ <Accordion title="Can a block ship widgets?">
94
+ 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.
95
+ </Accordion>
96
+ <Accordion title="Does the admin panel have widget zones too?">
97
+ 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.
98
+ </Accordion>
99
+ </AccordionGroup>
100
+
101
+ ## Next steps
102
+
103
+ <CardGroup cols={2}>
104
+ <Card title="Extend forms and tables" href="/rc/resources/tutorials/extend-forms-and-tables">
105
+ Add validated fields and columns with defineCustomFieldsConfig.
106
+ </Card>
107
+ <Card title="Extend the onboarding flow" href="/rc/resources/tutorials/extend-onboarding">
108
+ Add a store-setup field and persist it through a workflow hook.
109
+ </Card>
110
+ </CardGroup>
@@ -0,0 +1,113 @@
1
+ ---
2
+ title: "Add a button to order details"
3
+ description: "Drop a 'Copy link' button onto the vendor order detail page with a widget — no forking, no page override."
4
+ ---
5
+
6
+ The order detail page is a built-in panel screen you don't own. To add a small piece of UI to it — a button, a badge, a note — you don't copy the page. You drop a **widget** at one of its zones, and the SDK renders your component there while the rest of the page keeps working exactly as shipped.
7
+
8
+ This tutorial adds a **"Copy link"** button to the order summary section that copies a link to the order.
9
+
10
+ <Info>
11
+ **Additive, not a replacement.** A widget layers your component onto a built-in page at a documented zone. Reach for it first whenever you just want to *add* something to an existing screen.
12
+ </Info>
13
+
14
+ ## Add the button
15
+
16
+ <Steps>
17
+ <Step title="Create the widget file">
18
+ Drop a file under `src/widgets/`. Export the component as the **default** and a `config` built with `defineWidgetConfig`. Target `orders.detail.summary.after` — your component renders in the order summary section footer and receives the loaded order as `data`.
19
+
20
+ ```tsx apps/vendor/src/widgets/order-copy-link.tsx
21
+ import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
22
+ import type { HttpTypes } from "@medusajs/types"
23
+ import { Button, Container, toast } from "@medusajs/ui"
24
+
25
+ export const config = defineWidgetConfig({
26
+ zone: "orders.detail.summary.after",
27
+ })
28
+
29
+ const OrderCopyLink = ({ data: order }: { data?: HttpTypes.AdminOrder }) => {
30
+ if (!order) return null
31
+
32
+ const orderLink = `${window.location.origin}/orders/${order.id}`
33
+
34
+ const handleCopy = async () => {
35
+ try {
36
+ await navigator.clipboard.writeText(orderLink)
37
+ toast.success("Link copied")
38
+ } catch {
39
+ toast.error("Couldn't copy the link")
40
+ }
41
+ }
42
+
43
+ return (
44
+ <Container className="flex items-center justify-end p-3">
45
+ <Button size="small" variant="secondary" onClick={handleCopy}>
46
+ Copy link
47
+ </Button>
48
+ </Container>
49
+ )
50
+ }
51
+
52
+ export default OrderCopyLink
53
+ ```
54
+ </Step>
55
+ <Step title="Understand the zone id">
56
+ A zone id is `<domain>.<view>.<slot>.<placement>`. The last segment is the placement:
57
+
58
+ | Placement | Effect |
59
+ |-----------|--------|
60
+ | `before` | Renders before the built-in content of the zone |
61
+ | `after` | Renders after the built-in content |
62
+
63
+ Multiple `before` / `after` widgets on the same zone stack in registration order.
64
+ </Step>
65
+ <Step title="Reload the panel">
66
+ Start the project (`bun run dev`) and open any order in the vendor portal. Widget files hot-reload — the "Copy link" button appears in the summary section footer, and the rest of the page is untouched.
67
+ </Step>
68
+ </Steps>
69
+
70
+ ## Order detail zones
71
+
72
+ Zones mounted on the vendor order detail page:
73
+
74
+ | Zone | Where it renders |
75
+ |------|------------------|
76
+ | `orders.detail.summary.before` / `.after` | Inside the order summary section (around its footer) |
77
+ | `orders.detail.main.before` / `.after` | Around the main column (summary, payment, fulfillment) |
78
+ | `orders.detail.side.before` / `.after` | Around the sidebar (customer, activity) |
79
+
80
+ Each is passed the loaded `order` as `data`. The full, valid set is typed as `WidgetZoneId` and generated 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.
81
+
82
+ ## Verify
83
+
84
+ 1. Open an order — the "Copy link" button renders in the summary section footer.
85
+ 2. Click it — the link is copied and a toast appears.
86
+ 3. Change the zone to `orders.detail.side.before` and reload — the button moves to the top of the sidebar.
87
+ 4. Set `zone: "not.a.zone"` — `tsc` (`bun run lint`) fails with a "not assignable to `WidgetZoneId`" error.
88
+ 5. Delete the file — the button disappears; nothing else changed.
89
+
90
+ ## FAQ
91
+
92
+ <AccordionGroup>
93
+ <Accordion title="What can I read from the order?">
94
+ The zone passes the loaded order as `data` (`HttpTypes.AdminOrder`) — id, display id, totals, items, `payment_collections`, customer, and more. Build the link (or any UI) from it.
95
+ </Accordion>
96
+ <Accordion title="Can a block ship this instead of the host app?">
97
+ Yes. Put the same file in a [block](/rc/learn/blocks)'s `vendor_ui` entry under `src/widgets/`; installing the block adds the button with no wiring.
98
+ </Accordion>
99
+ <Accordion title="Can I render more than a button?">
100
+ The zone renders any React component — a badge, an action menu, a whole section. You have the full order in `data`.
101
+ </Accordion>
102
+ </AccordionGroup>
103
+
104
+ ## Next steps
105
+
106
+ <CardGroup cols={2}>
107
+ <Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
108
+ The general widget model and the full list of zones.
109
+ </Card>
110
+ <Card title="Extend forms and tables" href="/rc/resources/tutorials/extend-forms-and-tables">
111
+ Add validated fields and columns with defineCustomFieldsConfig.
112
+ </Card>
113
+ </CardGroup>
@@ -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>