@mercurjs/docs 2.2.0-canary.45 → 2.2.0-canary.47

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.
@@ -58,6 +58,16 @@ Mercur extends the Medusa server with three route surfaces. Start with the conve
58
58
  </Card>
59
59
  </CardGroup>
60
60
 
61
+ ## Panel extension API
62
+
63
+ The file-based helpers for extending the admin and vendor panels without forking.
64
+
65
+ <CardGroup cols={1}>
66
+ <Card title="Panel Extension API" href="/rc/references/panel-extension-api">
67
+ <code>defineWidgetConfig</code>, <code>defineNavigationConfig</code>, <code>defineCustomFieldsConfig</code>, and <code>createFormHelper</code> — config shapes and the exact props passed to every component you supply.
68
+ </Card>
69
+ </CardGroup>
70
+
61
71
  ## Workflows and configuration
62
72
 
63
73
  <CardGroup cols={2}>
@@ -0,0 +1,337 @@
1
+ ---
2
+ title: "Panel Extension API"
3
+ description: "The file-based helpers for extending the admin and vendor panels — defineWidgetConfig, defineNavigationConfig, defineCustomFieldsConfig, createFormHelper — and the exact props passed to every component you supply."
4
+ ---
5
+
6
+ This is the technical contract for the panel extension API.
7
+ Where the [Extend forms and tables](/rc/resources/tutorials/extend-forms-and-tables),
8
+ [Add a widget](/rc/resources/tutorials/add-a-widget), and
9
+ [Customize navigation](/rc/resources/tutorials/customize-navigation) tutorials
10
+ walk through *building* an extension, this page documents the exact config shape
11
+ of each helper and — most importantly — **the props each `component` you pass in
12
+ receives at render time**.
13
+
14
+ <Info>
15
+ **Separate apps, no `surface` field.** Admin (`@mercurjs/admin`, port 7000) and
16
+ vendor (`@mercurjs/vendor`, port 7001) are separate Vite apps. A file dropped
17
+ under a panel's `src/` targets *that* panel — the folder you author in *is* the
18
+ surface. The helpers are the same in both; import them from
19
+ `@mercurjs/dashboard-sdk` (config helpers) and `@mercurjs/dashboard-shared`
20
+ (`createFormHelper`).
21
+ </Info>
22
+
23
+ ## Typed targets (register once)
24
+
25
+ Zone ids, nav item ids, models, and built-in field ids are typed per panel from a
26
+ generated `extension-targets.d.ts`. Reference it once per host app so every
27
+ extension file type-checks with no per-file import:
28
+
29
+ ```typescript apps/vendor/src/extension-targets.d.ts
30
+ /// <reference types="@mercurjs/vendor/extension-targets" />
31
+ ```
32
+
33
+ ```typescript apps/admin-test/src/extension-targets.d.ts
34
+ /// <reference types="@mercurjs/admin/extension-targets" />
35
+ ```
36
+
37
+ A wrong `zone`, `model`, or nav `id` fails `tsc` (`bun run lint`) rather than
38
+ silently no-op'ing at runtime.
39
+
40
+ ---
41
+
42
+ ## `defineWidgetConfig`
43
+
44
+ A widget is a React component attached to a named zone on a built-in page. The
45
+ placement (`before | after | replace`) is the last segment of the zone id.
46
+
47
+ ```tsx apps/vendor/src/widgets/product-list-banner.tsx
48
+ import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
49
+
50
+ export const config = defineWidgetConfig({
51
+ zone: "product.list.before", // WidgetZoneId — <domain>.<view>[.<slot>].<before|after|replace>
52
+ })
53
+
54
+ export default ProductListBanner
55
+ ```
56
+
57
+ ### Config
58
+
59
+ | Field | Type | Description |
60
+ | ------ | ------------------------------- | --------------------------------------------------------------------------- |
61
+ | `zone` | `WidgetZoneId \| WidgetZoneId[]` | Target zone(s). Placement is the suffix. Multiple widgets stack in order. |
62
+ | `id` | `string` (optional) | Stable id; derived from the file path at build time when omitted. |
63
+
64
+ ### Component props
65
+
66
+ The widget component receives a single prop:
67
+
68
+ | Prop | Type | Description |
69
+ | ------ | --------- | ----------------------------------------------------------------------------------------- |
70
+ | `data` | `unknown` | The zone's contextual entity — e.g. the loaded product on a `product.detail.*` zone. Undefined on list/public zones that have no single entity. |
71
+
72
+ ```tsx
73
+ const ProductDetailNote = ({ data }: { data?: HttpTypes.AdminProduct }) => (
74
+ <Container>{data?.title}</Container>
75
+ )
76
+ ```
77
+
78
+ <Note>
79
+ The public `login.logo` / `login.before` / `login.after` zones render before
80
+ authentication and receive no `data`.
81
+ </Note>
82
+
83
+ ---
84
+
85
+ ## `defineNavigationConfig`
86
+
87
+ Reorder, hide, relabel, or re-parent **built-in** sidebar items. Single
88
+ host-owned file — blocks cannot contribute nav overrides.
89
+
90
+ ```ts apps/vendor/src/_navigation.ts
91
+ import { defineNavigationConfig } from "@mercurjs/dashboard-sdk"
92
+
93
+ export default defineNavigationConfig({
94
+ items: [
95
+ { id: "orders", rank: 0 },
96
+ { id: "price-lists", hidden: true },
97
+ { id: "payouts", label: "settlements" },
98
+ { id: "categories", nested: null, rank: 1 },
99
+ { id: "campaigns", nested: "orders" },
100
+ ],
101
+ })
102
+ ```
103
+
104
+ ### `NavItemOverride`
105
+
106
+ | Field | Type | Description |
107
+ | -------- | -------------------------- | ------------------------------------------------------------------------------ |
108
+ | `id` | `NavItemId` | Built-in item to override (top-level or nested). Typed against the panel registry. |
109
+ | `rank` | `number` (optional) | Order within the item's parent (or among top-level items). |
110
+ | `hidden` | `boolean` (optional) | Remove from the sidebar (route may still be directly reachable). |
111
+ | `label` | `string` (optional) | i18n key or literal replacing the item's label. |
112
+ | `icon` | `ComponentType` (optional) | Icon component replacing the item's icon (from `@medusajs/icons`). |
113
+ | `nested` | `NavParentId \| null` (optional) | Re-parent under a built-in parent id; `null` promotes a nested item to top level. |
114
+
115
+ There is no `component` prop here — navigation overrides reshape existing items
116
+ only.
117
+
118
+ ---
119
+
120
+ ## `defineCustomFieldsConfig`
121
+
122
+ The model-scoped surface. One file per model adds form fields, section displays,
123
+ section actions, and list-table columns. Import
124
+ `createFormHelper` from `@mercurjs/dashboard-shared` to turn a Zod schema into an
125
+ input type + validation.
126
+
127
+ ```tsx apps/vendor/src/custom-fields/product.tsx
128
+ import { defineCustomFieldsConfig } from "@mercurjs/dashboard-sdk"
129
+ import { createFormHelper } from "@mercurjs/dashboard-shared"
130
+
131
+ type ProductWithMeta = { metadata?: Record<string, unknown> }
132
+ const form = createFormHelper<ProductWithMeta>()
133
+
134
+ export default defineCustomFieldsConfig({
135
+ model: "product",
136
+ link: "brand", // string | string[] — module link(s) fetched alongside the entity
137
+ forms: [/* ... */],
138
+ displays: [/* ... */],
139
+ list: {/* ... */},
140
+ })
141
+ ```
142
+
143
+ ### Top-level config
144
+
145
+ | Field | Type | Description |
146
+ | ---------- | ------------------------ | ------------------------------------------------------------------------ |
147
+ | `model` | `CustomFieldModel` | Target model (start with `"product"`). Typed against `CustomFieldsRegistry`. |
148
+ | `link` | `string \| string[]` | Module link(s) fetched alongside the entity; their data is available to columns and displays via the `+link.*` fetch. |
149
+ | `forms` | `CustomFormEntry[]` | Fields injected into built-in create/edit/onboarding forms. |
150
+ | `displays` | `CustomDisplayEntry[]` | Field replace/remove/add + `ActionMenu` actions on detail sections. |
151
+ | `list` | `CustomListExtension` | Columns, bulk actions, filters, view defaults on the model's list table. |
152
+
153
+ ### `forms[]` — form fields
154
+
155
+ ```tsx
156
+ forms: [
157
+ {
158
+ zone: "edit", // CustomFormZone — "create" | "edit" | "organize" | "attributes" | "onboarding"
159
+ tab: "general", // TabbedForm tab id, or wizard step id for zone: "onboarding"
160
+ fields: {
161
+ erp_id: form.define({
162
+ validation: form.string().nullish(), // Zod → input type + validation
163
+ defaultValue: (data) => (data?.metadata?.erp_id as string) ?? "",
164
+ label: "ERP ID",
165
+ description: "External system identifier",
166
+ placeholder: "ERP-000",
167
+ component: MyErpInput, // optional custom render
168
+ }),
169
+ },
170
+ },
171
+ ]
172
+ ```
173
+
174
+ Each field is a `CustomFormField`:
175
+
176
+ | Field | Type | Description |
177
+ | -------------- | ---------------------------------------- | ----------------------------------------------------------------- |
178
+ | `validation` | Zod schema | Drives both the default input type and validation. |
179
+ | `defaultValue` | `unknown \| ((data) => unknown)` | Static value or a resolver from the loaded entity. |
180
+ | `label` | `string` (optional) | Field label. |
181
+ | `description` | `string` (optional) | Help text below the input. |
182
+ | `placeholder` | `string` (optional) | Input placeholder. |
183
+ | `component` | `ComponentType` (optional) | Custom render; falls back to a default input for the Zod type. |
184
+
185
+ <Warning>
186
+ **Form-field `component` receives no props.** It is rendered as `<Component />`
187
+ inside the field's `additional_data.<field>` React Hook Form context. Read and
188
+ write the value with RHF's `useFormContext()` / `useController()`, and render
189
+ through the mandated `Form.Field → Form.Item` chain — do not use a raw
190
+ `Controller`. Values live in form state under `additional_data`; in the MVP,
191
+ `product` custom fields persist onto the product's `metadata`.
192
+ </Warning>
193
+
194
+ ### `displays[]` — detail sections
195
+
196
+ ```tsx
197
+ displays: [
198
+ {
199
+ zone: "general", // CustomFieldsRegistry displayZones — an existing detail section id
200
+ fields: [
201
+ { id: "erp_id", component: ErpRow }, // ADD (unknown id → new read-only row)
202
+ { id: "status", component: BrandedStatusBadge }, // REPLACE a built-in field's render
203
+ { id: "created_by", component: null }, // REMOVE a built-in field
204
+ ],
205
+ actions: [
206
+ { rank: 0, component: SyncErpAction }, // add to the section's ActionMenu
207
+ ],
208
+ },
209
+ ]
210
+ ```
211
+
212
+ `fields[]` is `CustomDisplayField`:
213
+
214
+ | Field | Type | Description |
215
+ | ----------- | --------------------------------------- | -------------------------------------------------------------------------------------- |
216
+ | `id` | `displayFieldIds \| (string & {})` | Built-in field id (autocompletes) → replace/remove; any other string → add a new row. |
217
+ | `component` | `ComponentType<{ data? }> \| null` | Render component, or `null` to remove a built-in field. |
218
+
219
+ **Display field `component` props:**
220
+
221
+ | Prop | Type | Description |
222
+ | ------ | --------- | ---------------------------------- |
223
+ | `data` | `unknown` | The loaded detail entity, **including any `link`ed module data**. If the config declares `link: "brand"`, read it off `data.brand` here. |
224
+
225
+ ```tsx
226
+ // with `link: "brand"` on the config, the linked module data rides on `data`
227
+ const BrandRow = ({ data }: { data?: { brand?: { name: string } } }) => (
228
+ <Text>{data?.brand?.name}</Text>
229
+ )
230
+ ```
231
+
232
+ `actions[]` is `SectionAction` (the same shape as list `bulkActions`):
233
+
234
+ | Field | Type | Description |
235
+ | ----------- | ------------------------------- | ----------------------------------------------------------------- |
236
+ | `rank` | `number` (optional) | Position within the section's `ActionMenu`. |
237
+ | `component` | `ComponentType<{ data? }>` | Owns its own label, icon, group placement, and `onClick`. |
238
+
239
+ **Section-action `component` props:**
240
+
241
+ | Prop | Type | Description |
242
+ | ------ | --------- | ----------------------------------------------------------------------- |
243
+ | `data` | `unknown` | The loaded detail entity, **including any `link`ed module data** (e.g. `data.brand`). |
244
+
245
+ ### `list` — list table
246
+
247
+ ```tsx
248
+ list: {
249
+ columns: [
250
+ { id: "title", component: ({ value }) => <strong>{value}</strong> }, // override a cell
251
+ { id: "brand_name", header: "Brand", component: ({ row }) => row.brand?.name }, // add (from link)
252
+ ],
253
+ bulkActions: [{ rank: 0, component: ArchiveBulkAction }],
254
+ filters: [/* add / remove list filters */],
255
+ viewDefaults: {
256
+ columnVisibility: { created_at: false }, // hide a column
257
+ columnOrder: ["title", "sku", "erp_id"],
258
+ },
259
+ }
260
+ ```
261
+
262
+ `columns[]` is `CustomColumn`:
263
+
264
+ | Field | Type | Description |
265
+ | ----------- | ------------------------------------------ | -------------------------------------------------------- |
266
+ | `id` | `string` | Column id — matches a built-in column to override, or adds a new one. |
267
+ | `header` | `string` (optional) | Column header text (for added columns). |
268
+ | `component` | `ComponentType<{ row?, value? }>` (optional) | Cell renderer. |
269
+
270
+ **Column `component` props:**
271
+
272
+ | Prop | Type | Description |
273
+ | ------- | --------- | -------------------------------------------------------- |
274
+ | `row` | `unknown` | The full row entity (including `link`ed module data). |
275
+ | `value` | `unknown` | The cell value for this column id, when derivable. |
276
+
277
+ `bulkActions[]` is `SectionAction` (`{ rank?, component }`, component props `{ data? }`).
278
+
279
+ <Note>
280
+ **Bulk-action rendering is deferred in the MVP.** `bulkActions` are accepted and
281
+ surfaced by the config but not yet mounted into the list toolbar.
282
+ </Note>
283
+
284
+ <Warning>
285
+ **Vendor product list is field-constrained.** The vendor `product` list must use
286
+ the curated fields from `useProductTableQuery` — the SDK merges `link` fetches
287
+ with the `+`/`-` convention, never bare fields, or the list 500s. You never
288
+ hand-write the field list; the `link` declaration drives it.
289
+ </Warning>
290
+
291
+ ---
292
+
293
+ ## `createFormHelper`
294
+
295
+ `createFormHelper<T>()` returns Medusa's zod surface for describing field
296
+ validation and value types:
297
+
298
+ ```ts
299
+ const form = createFormHelper<ProductWithMeta>()
300
+
301
+ form.define({ validation, defaultValue?, label?, description?, placeholder?, component? })
302
+ form.string() / form.number() / form.boolean() / form.date()
303
+ form.array() / form.object() / form.null() / form.nullable() / form.coerce
304
+ ```
305
+
306
+ The generated registry types the **target** (which zone/tab/field ids exist); the
307
+ Zod `validation` types the **value**. The two compose — codegen types the address,
308
+ Zod types the payload.
309
+
310
+ ---
311
+
312
+ ## Persistence
313
+
314
+ <Warning>
315
+ **The MVP is a UI surface only.** Custom fields render, validate, and display
316
+ through the built-in forms/sections/tables — there is no generic core-side write
317
+ path. For `product`, values are submitted under `additional_data` and persisted
318
+ onto `metadata`. To store data for other models, wire your own route/workflow or
319
+ use the backend [Custom Fields module](/rc/resources/customization/custom-fields).
320
+ </Warning>
321
+
322
+ ## Related
323
+
324
+ <CardGroup cols={2}>
325
+ <Card title="Extend forms and tables" href="/rc/resources/tutorials/extend-forms-and-tables">
326
+ Step-by-step build with `defineCustomFieldsConfig`.
327
+ </Card>
328
+ <Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
329
+ Inject a component into a zone.
330
+ </Card>
331
+ <Card title="Customize navigation" href="/rc/resources/tutorials/customize-navigation">
332
+ Reorder and hide sidebar items.
333
+ </Card>
334
+ <Card title="Custom Fields module" href="/rc/references/modules/custom-fields">
335
+ The backend storage layer.
336
+ </Card>
337
+ </CardGroup>
@@ -70,7 +70,7 @@ Zones mounted today in the **vendor portal**:
70
70
  | Zone | Where it renders |
71
71
  |------|------------------|
72
72
  | `product.list.before` / `.after` | Vendor product list page |
73
- | `store.setup.before` / `.after` | The store-setup / onboarding surface (dashboard home + store settings), passed the `seller` as `data` |
73
+ | `seller.setup.before` / `.after` | The store-setup / onboarding surface (dashboard home + store settings), passed the `seller` as `data` |
74
74
  | `login.logo.*` | The logo slot on the public login screen |
75
75
  | `login.before.*` / `login.after.*` | Around the login form (rendered before authentication) |
76
76
 
@@ -3,10 +3,10 @@ title: "Extend the onboarding flow"
3
3
  description: "Add a field to the vendor store-setup surface, submit it through additional_data, and persist it durably from a workflow hook — end to end, without forking."
4
4
  ---
5
5
 
6
- The vendor **store-setup / onboarding** surface is a widget zone (`store.setup`) that renders the full `seller` object as its `data`. That makes onboarding a full extension seam: drop a widget to add UI, carry the new value to the API on the built-in seller routes through `additional_data`, and persist it from a workflow hook — the same three layers you'd wire in plain Medusa, kept intact by Mercur.
6
+ The vendor **store-setup / onboarding** surface is a widget zone (`seller.setup`) that renders the full `seller` object as its `data`. That makes onboarding a full extension seam: drop a widget to add UI, carry the new value to the API on the built-in seller routes through `additional_data`, and persist it from a workflow hook — the same three layers you'd wire in plain Medusa, kept intact by Mercur.
7
7
 
8
8
  <Info>
9
- **Three layers, one flow.** The panel (a `store.setup` widget) *renders and collects*. The vendor seller route *carries* the value through `additional_data` — no core schema change. A `sellersUpdated` workflow hook *persists* it. Each layer is additive: nothing built-in is replaced.
9
+ **Three layers, one flow.** The panel (a `seller.setup` widget) *renders and collects*. The vendor seller route *carries* the value through `additional_data` — no core schema change. A `sellersUpdated` workflow hook *persists* it. Each layer is additive: nothing built-in is replaced.
10
10
  </Info>
11
11
 
12
12
  ## What you'll build
@@ -21,13 +21,13 @@ Widget zones are typed ids the vendor panel generates from its own pages and shi
21
21
  /// <reference types="@mercurjs/vendor/extension-targets" />
22
22
  ```
23
23
 
24
- With it present, `store.setup` autocompletes and an invalid zone fails `tsc` instead of silently doing nothing.
24
+ With it present, `seller.setup` autocompletes and an invalid zone fails `tsc` instead of silently doing nothing.
25
25
 
26
26
  ## 1 — Render on the onboarding surface
27
27
 
28
28
  <Steps>
29
29
  <Step title="Add the store-setup widget">
30
- Drop a file under `src/widgets/`. Export the component as the **default** and a `config` with `zone: "store.setup.before"`. The zone hands your component the `seller` as `data`:
30
+ Drop a file under `src/widgets/`. Export the component as the **default** and a `config` with `zone: "seller.setup.before"`. The zone hands your component the `seller` as `data`:
31
31
 
32
32
  ```tsx apps/vendor/src/widgets/tax-id-setup.tsx
33
33
  import "@mercurjs/vendor/extension-targets"
@@ -40,7 +40,7 @@ With it present, `store.setup` autocompletes and an invalid zone fails `tsc` ins
40
40
  import { client } from "../lib/client"
41
41
 
42
42
  export const config = defineWidgetConfig({
43
- zone: "store.setup.before",
43
+ zone: "seller.setup.before",
44
44
  })
45
45
 
46
46
  const TaxIdSetup = ({ data: seller }: { data?: SellerDTO }) => {
@@ -94,14 +94,14 @@ With it present, `store.setup` autocompletes and an invalid zone fails `tsc` ins
94
94
  ```
95
95
  </Step>
96
96
  <Step title="Understand where it renders">
97
- `store.setup` is hosted in two places, both passing the same `seller` as `data`:
97
+ `seller.setup` is hosted in two places, both passing the same `seller` as `data`:
98
98
 
99
99
  | Host | When it shows |
100
100
  |------|---------------|
101
101
  | The vendor shell (above the page outlet) | On top-level routes — the dashboard "home" onboarding banner |
102
102
  | The store settings detail page | Always, above the store status banner |
103
103
 
104
- A single widget file covers both. Multiple `store.setup.before` / `.after` widgets stack in registration order.
104
+ A single widget file covers both. Multiple `seller.setup.before` / `.after` widgets stack in registration order.
105
105
  </Step>
106
106
  </Steps>
107
107
 
@@ -203,7 +203,7 @@ That is the whole "wiring" step: your `{ tax_id }` payload arrives in the workfl
203
203
  ## How the layers connect
204
204
 
205
205
  ```
206
- store.setup widget → client.vendor.sellers.$id.mutate({ additional_data })
206
+ seller.setup widget → client.vendor.sellers.$id.mutate({ additional_data })
207
207
 
208
208
 
209
209
  POST /vendor/sellers/:id → updateSellersWorkflow({ update, additional_data })
@@ -0,0 +1,214 @@
1
+ ---
2
+ title: "Build a store-setup checklist"
3
+ description: "Render an onboarding progress checklist on the vendor store-setup surface with a seller.setup widget — the seller is handed to you as data, no fetching required."
4
+ ---
5
+
6
+ The vendor **store-setup / onboarding** surface is a widget zone (`seller.setup`)
7
+ that renders the full `seller` object as its `data`. That makes it the perfect
8
+ seam for an onboarding **checklist**: derive completion from the seller you're
9
+ handed, show what's left, and link each step to the page that completes it — all
10
+ from a single widget file, with nothing fetched and nothing forked.
11
+
12
+ <Info>
13
+ **This is the pattern the built-in store-setup card uses.** The shipped
14
+ "Complete store profile" card is itself a `seller.setup` widget that reads the
15
+ `seller` and renders a progress list. This tutorial rebuilds it so you can
16
+ shape your own onboarding steps.
17
+ </Info>
18
+
19
+ ## What you'll build
20
+
21
+ A collapsible progress card on the store-setup surface that checks four parts of
22
+ the seller profile (details, address, company, payment), draws a progress bar,
23
+ and routes the vendor to the settings page for each incomplete step. When every
24
+ step is done, the card removes itself.
25
+
26
+ ## Register the typed targets (once)
27
+
28
+ Widget zones are typed ids the vendor panel generates from its own pages and
29
+ ships as `@mercurjs/vendor/extension-targets`. Reference them once so `seller.setup`
30
+ autocompletes and a wrong zone fails `tsc`:
31
+
32
+ ```typescript apps/vendor/src/extension-targets.d.ts
33
+ /// <reference types="@mercurjs/vendor/extension-targets" />
34
+ ```
35
+
36
+ ## Build the checklist widget
37
+
38
+ <Steps>
39
+ <Step title="Derive the steps from the seller">
40
+ The `data` prop is the full `SellerDTO`. Compute each step's `completed` flag
41
+ from it — no request needed. Give every step a `path` to the settings page
42
+ that finishes it:
43
+
44
+ ```tsx apps/vendor/src/widgets/store-setup-checklist.tsx
45
+ import "@mercurjs/vendor/extension-targets"
46
+ import { SellerDTO } from "@mercurjs/types"
47
+
48
+ type ProfileStep = {
49
+ key: string
50
+ label: string
51
+ completed: boolean
52
+ path: string
53
+ }
54
+
55
+ function getProfileSteps(seller: SellerDTO): ProfileStep[] {
56
+ const hasStoreDetails = !!(seller.name && seller.email && seller.description)
57
+ const hasAddress = !!(
58
+ seller.address?.address_1 &&
59
+ seller.address?.city &&
60
+ seller.address?.country_code
61
+ )
62
+ const hasCompanyDetails = !!seller.professional_details?.corporate_name
63
+ const hasPaymentDetails = !!(
64
+ seller.payment_details?.holder_name &&
65
+ seller.payment_details?.country_code
66
+ )
67
+
68
+ return [
69
+ { key: "store_details", label: "Add store details", completed: hasStoreDetails, path: "/settings/store/edit" },
70
+ { key: "address", label: "Add address", completed: hasAddress, path: "/settings/store/address" },
71
+ { key: "company_details", label: "Add company details", completed: hasCompanyDetails, path: "/settings/store/professional-details" },
72
+ { key: "payment_details", label: "Add payment details", completed: hasPaymentDetails, path: "/settings/store/payment-details" },
73
+ ]
74
+ }
75
+ ```
76
+
77
+ </Step>
78
+ <Step title="Render the collapsible progress card">
79
+ Use `@medusajs/ui` primitives and `@medusajs/icons` only. Draw a progress bar
80
+ from the completed ratio, list each step with a status icon, and navigate on
81
+ click. Return `null` when everything is done so the card disappears:
82
+
83
+ ```tsx apps/vendor/src/widgets/store-setup-checklist.tsx
84
+ import { useMemo, useState } from "react"
85
+ import { useNavigate } from "react-router-dom"
86
+ import { Container, Text, clx } from "@medusajs/ui"
87
+ import { CheckCircleSolid, TriangleDownMini, CircleDottedLine } from "@medusajs/icons"
88
+ import { Collapsible as RadixCollapsible } from "radix-ui"
89
+
90
+ const StoreSetupChecklist = ({ data: seller }: { data?: SellerDTO }) => {
91
+ const navigate = useNavigate()
92
+ const [open, setOpen] = useState(true)
93
+
94
+ const steps = useMemo(
95
+ () => (seller ? getProfileSteps(seller) : []),
96
+ [seller]
97
+ )
98
+
99
+ if (!seller) {
100
+ return null
101
+ }
102
+
103
+ const completedCount = steps.filter((s) => s.completed).length
104
+ const totalCount = steps.length
105
+ const progressPercent = (completedCount / totalCount) * 100
106
+
107
+ if (completedCount === totalCount) {
108
+ return null
109
+ }
110
+
111
+ return (
112
+ <RadixCollapsible.Root open={open} onOpenChange={setOpen}>
113
+ <Container className="overflow-hidden p-0">
114
+ <div
115
+ className="h-1 bg-ui-tag-green-icon transition-all duration-500"
116
+ style={{ width: `${progressPercent}%` }}
117
+ />
118
+ <div className="p-6">
119
+ <RadixCollapsible.Trigger asChild>
120
+ <button className="flex w-full items-center justify-between">
121
+ <Text size="large" weight="plus" leading="compact">
122
+ Complete store profile
123
+ </Text>
124
+ <TriangleDownMini
125
+ className={clx(
126
+ "text-ui-fg-muted transition-transform duration-200",
127
+ !open && "-rotate-90"
128
+ )}
129
+ />
130
+ </button>
131
+ </RadixCollapsible.Trigger>
132
+
133
+ <RadixCollapsible.Content>
134
+ <div className="mt-4 flex flex-col gap-y-3">
135
+ {steps.map((step) => (
136
+ <button
137
+ key={step.key}
138
+ className="flex items-center gap-x-3 text-left"
139
+ onClick={() => !step.completed && navigate(step.path)}
140
+ disabled={step.completed}
141
+ >
142
+ {step.completed ? (
143
+ <CheckCircleSolid className="text-ui-tag-green-icon shrink-0" />
144
+ ) : (
145
+ <CircleDottedLine className="text-ui-fg-muted shrink-0" />
146
+ )}
147
+ <Text size="small" leading="compact" className="text-ui-fg-base">
148
+ {step.label}
149
+ </Text>
150
+ </button>
151
+ ))}
152
+ </div>
153
+ </RadixCollapsible.Content>
154
+ </div>
155
+ </Container>
156
+ </RadixCollapsible.Root>
157
+ )
158
+ }
159
+ ```
160
+
161
+ </Step>
162
+ <Step title="Attach it to the store-setup zone">
163
+ Export the component as the **default** and a `config` with a `seller.setup`
164
+ zone. `before` renders it above the built-in card; use `after` to place it
165
+ below:
166
+
167
+ ```tsx apps/vendor/src/widgets/store-setup-checklist.tsx
168
+ import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
169
+
170
+ export const config = defineWidgetConfig({
171
+ zone: "seller.setup.before",
172
+ })
173
+
174
+ export default StoreSetupChecklist
175
+ ```
176
+
177
+ </Step>
178
+ </Steps>
179
+
180
+ ## Where it renders
181
+
182
+ `seller.setup` is hosted in two places, both passing the same `seller` as `data`:
183
+
184
+ | Host | When it shows |
185
+ | ---------------------------------------- | ------------------------------------------------------------ |
186
+ | The vendor shell (above the page outlet) | On top-level routes — the dashboard "home" onboarding banner |
187
+ | The store settings detail page | Always, above the store status banner |
188
+
189
+ A single widget file covers both. Multiple `seller.setup.before` / `.after`
190
+ widgets stack in registration order, so your checklist and the built-in card can
191
+ coexist — or hide the built-in one by rendering your own and letting theirs
192
+ complete.
193
+
194
+ <Note>
195
+ **No data fetching.** Because the zone hands you the resolved `seller`, the
196
+ widget is pure render — you never call the SDK. To *collect and persist* a new
197
+ onboarding value (e.g. a Tax ID) rather than just link to existing pages, see
198
+ [Extend the onboarding flow](/rc/resources/tutorials/extend-onboarding), which
199
+ carries a value through `additional_data` to a workflow hook.
200
+ </Note>
201
+
202
+ ## Next steps
203
+
204
+ <CardGroup cols={2}>
205
+ <Card
206
+ title="Extend the onboarding flow"
207
+ href="/rc/resources/tutorials/extend-onboarding"
208
+ >
209
+ Collect a new value and persist it through a workflow hook.
210
+ </Card>
211
+ <Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
212
+ The full widget model and the published zone registry.
213
+ </Card>
214
+ </CardGroup>
package/llms.txt CHANGED
@@ -50,6 +50,7 @@ package (`node_modules/@mercurjs/docs/`).
50
50
  - [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.
51
51
  - [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.
52
52
  - [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.
53
+ - [Build a store-setup checklist](content/resources/tutorials/store-setup-checklist.mdx) — Render an onboarding progress checklist on the vendor store-setup surface with a seller.setup widget — the seller is handed to you as data, no fetching required.
53
54
 
54
55
  ## Tools — CLI, API client, dashboard SDK
55
56
 
@@ -215,6 +216,7 @@ package (`node_modules/@mercurjs/docs/`).
215
216
  - [Search module](content/references/modules/search.mdx) — The provider-agnostic search module, its document shapes, and the provider contract.
216
217
  - [Seller module](content/references/modules/seller.mdx) — Data models, links, and service methods for the Seller module.
217
218
  - [References](content/references/overview.mdx) — Technical references for Mercur's modules, HTTP APIs, workflows, and configuration.
219
+ - [Panel Extension API](content/references/panel-extension-api.mdx) — The file-based helpers for extending the admin and vendor panels — defineWidgetConfig, defineNavigationConfig, defineCustomFieldsConfig, createFormHelper — and the exact props passed to every component you supply.
218
220
  - [addSellerShippingMethodToCartWorkflow](content/references/workflows/cart/add-seller-shipping-method-to-cart.mdx) — Add shipping methods to a cart, replacing only the same seller's existing methods.
219
221
  - [completeCartWithSplitOrdersWorkflow](content/references/workflows/cart/complete-cart-with-split-orders.mdx) — Complete a multi-seller cart by splitting it into per-seller orders under an order group.
220
222
  - [listSellerShippingOptionsForCartWorkflow](content/references/workflows/cart/list-seller-shipping-options-for-cart.mdx) — List available shipping options for a cart, grouped by seller.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercurjs/docs",
3
- "version": "2.2.0-canary.45",
3
+ "version": "2.2.0-canary.47",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/mercurjs/mercur",