@mercurjs/docs 2.3.6-canary.8 → 2.3.6

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.
@@ -116,7 +116,7 @@ The 1.x `OrderSet` entity is renamed to `OrderGroup` in 2.x. This is a breaking
116
116
 
117
117
  Two fields were removed from the group:
118
118
 
119
- - **`payment_collection_id`**: payment collections are now linked at the individual order level. Query the linked orders to get the payment collection.
119
+ - **`payment_collection_id`**: orders share the cart's payment collection until the payment is captured, then each gets its own. Read `payment_collections` on the orders (see [Order Groups](/learn/order-groups)).
120
120
  - **`sales_channel_id`**: the sales channel is stored on each individual order.
121
121
 
122
122
  Two fields are now computed at query time rather than stored: **`seller_count`** (distinct sellers across linked orders) and **`total`** (sum of order totals).
@@ -54,16 +54,39 @@ Order groups are created automatically during checkout by the `completeCartWithS
54
54
  - Each seller and the customer (if new relationship)
55
55
  6. **Finalize** — Inventory is reserved, payment is authorized, and the `order_group.created` event is emitted
56
56
 
57
- <Info>
58
- A single payment collection is shared across all of a cart's split orders and stays on the **cart**, not on each order. Split orders are **not** linked directly to the payment collection (Medusa's `order ↔ payment_collection` link is one-to-one on the payment-collection side). Read the payment collection through the cart:
57
+ ## Payments
58
+
59
+ The customer pays for the cart once: one payment collection on the cart, one payment at the payment provider. Medusa links a payment collection to a single order, so the orders of a cart share that payment through the cart until it is captured:
60
+
61
+ ```text
62
+ order → cart → payment_collection → payment
63
+ ```
64
+
65
+ When the cart payment is captured, it is split. Every order gets a payment collection of its own, linked to the order, with a captured payment for the order's share. That payment is on the same provider as the cart payment and points at the same charge. Shares are proportional to the order totals, rounded to the currency precision, and sum exactly to the captured amount.
59
66
 
60
- ```text
61
- order → cart → payment_collection
62
- ```
67
+ ```text
68
+ order → payment_collections → payment
69
+ ```
63
70
 
64
- Order-group and vendor order reads normalize `cart.payment_collection` back onto `payment_collections` and recompute `payment_status` from it, so consumers keep the familiar shape. When requesting fields on vendor order routes, make sure the request does not replace route defaults that already include `cart.payment_collection.*` (a `fields=` list containing bare fields replaces defaults in Medusa; use `+`-prefixed fields to merge, or include the `cart.payment_collection.*` paths explicitly).
71
+ From then on Medusa's order and payment workflows and the stock admin and vendor routes work on the order unchanged.
72
+
73
+ | Moment | What happens |
74
+ |--------|--------------|
75
+ | Cancel an order before capture | No payment is touched: the authorization is still needed by the cart's other orders. Once every order of the cart is canceled, the authorization is voided. |
76
+ | Capture | The cart payment is captured as a whole, by the provider webhook or from the dashboard, and split between the orders. What was charged for orders canceled earlier is refunded. |
77
+ | Refund an order's payment | The amount is refunded at the provider against the customer's charge and recorded on that order. It cannot exceed the order's share. |
78
+ | Cancel an order after capture | The order's share is refunded. |
79
+
80
+ Order reads expose whichever applies under `payment_collections`: the cart's collection before the capture, the order's own afterwards.
81
+
82
+ <Info>
83
+ The split, and voiding the authorization after the last cancel, run in subscribers to `payment.captured` and `order.canceled`, shortly after the request returns. A provider that captures on authorization is split during checkout.
65
84
  </Info>
66
85
 
86
+ <Note>
87
+ Orders whose payment was captured before this model was introduced are not split retroactively and keep the cart's shared collection.
88
+ </Note>
89
+
67
90
  ## Events
68
91
 
69
92
  | Event | Payload | When |
@@ -42,6 +42,10 @@ The workflow returns `{ rows, metadata }`, where `metadata` carries the `count`,
42
42
  Filters go inside `variables`: the group repository understands `id`,
43
43
  `customer_id`, `seller_id`, `status`, `sales_channel_id`, `created_at`,
44
44
  `updated_at`, and a free-text `q` (matched against group id and customer id).
45
+ Order-level filters (`seller_id`, `status`, `sales_channel_id`) only decide
46
+ which groups match. A matching group's `seller_count` and `total` still
47
+ describe every order in the group, as covered in
48
+ [Computed totals](/platform/order-group/concepts/computed-totals).
45
49
  </Note>
46
50
 
47
51
  ## Scope to a seller
@@ -4,7 +4,7 @@ api: "POST /vendor/sellers/select"
4
4
  description: "Set the active seller for the member's session."
5
5
  ---
6
6
 
7
- Stores the chosen seller in the session after verifying the member belongs to it.
7
+ Verifies the member belongs to the chosen seller. For session-authenticated requests the seller is also stored in the session.
8
8
 
9
9
  <Note>
10
10
  Requires only the `Authorization` header. Session-based clients get the seller stored server-side; token-based clients should send `x-seller-id` on subsequent requests instead.
@@ -22,6 +22,10 @@ Stores the chosen seller in the session after verifying the member belongs to it
22
22
  Whether the seller was selected.
23
23
  </ResponseField>
24
24
 
25
+ <ResponseField name="seller_id" type="string">
26
+ The selected seller's ID. Token-based clients send it as `x-seller-id` on subsequent requests.
27
+ </ResponseField>
28
+
25
29
  <RequestExample>
26
30
  ```bash cURL
27
31
  curl -X POST 'http://localhost:9000/vendor/sellers/select' \
@@ -31,7 +35,7 @@ curl -X POST 'http://localhost:9000/vendor/sellers/select' \
31
35
  ```
32
36
 
33
37
  ```ts JS Client
34
- const { success } = await client.vendor.sellers.select.mutate({
38
+ const { seller_id } = await client.vendor.sellers.select.mutate({
35
39
  seller_id: "sel_01HXYZ",
36
40
  })
37
41
  ```
@@ -39,6 +43,6 @@ const { success } = await client.vendor.sellers.select.mutate({
39
43
 
40
44
  <ResponseExample>
41
45
  ```json 200
42
- { "success": true }
46
+ { "success": true, "seller_id": "sel_01HXYZ" }
43
47
  ```
44
48
  </ResponseExample>
@@ -70,6 +70,7 @@ The starter (`apps/api` and projects created with `create-mercur-app`) reads:
70
70
  | `COOKIE_SECRET` | Cookie signing secret | `supersecret` (change in production) |
71
71
  | `FILE_BACKEND_URL` | Public origin baked into uploaded file URLs | `http://localhost:9000/static` |
72
72
  | `MERCUR_VENDOR_URL` | Vendor dashboard base URL used in member invite links | `""`; can also be set via the seller module's `vendor_url` option |
73
+ | `MERCUR_DEV_CODEGEN` | Set to `"false"` to skip automatic type generation on dev startup | `"true"` |
73
74
  | `NODE_ENV` | Environment selection | `development` |
74
75
 
75
76
  ## Dashboard modules
@@ -101,6 +101,7 @@ The `config` object controls the page's sidebar entry.
101
101
  | `icon` | `ComponentType` (optional) | Sidebar icon, from `@medusajs/icons`. |
102
102
  | `rank` | `number` (optional) | Order among sibling items. Lower ranks first. |
103
103
  | `nested` | `string` (optional) | Parent item id to nest this entry under. |
104
+ | `group` | `string` (optional) | Sidebar group to list the entry under, as a string literal. See [Groups](#groups). |
104
105
  | `translationNs` | `string` (optional) | i18n namespace used to resolve `label` as a translation key. |
105
106
  | `public` | `boolean` (optional) | Render the route before authentication under the public layout. |
106
107
 
@@ -131,7 +132,7 @@ Read the loader's result with `useLoaderData()` inside the component.
131
132
 
132
133
  ## Reshape built-in navigation
133
134
 
134
- Use `defineNavigationConfig` to reorder, hide, relabel, or re-parent built-in
135
+ Use `defineNavigationConfig` to reorder, hide, relabel, re-parent, or group built-in
135
136
  sidebar items. It lives in a single host-owned file, `src/_navigation.ts`. Blocks
136
137
  cannot contribute navigation overrides.
137
138
 
@@ -159,6 +160,49 @@ Each entry is a `NavItemOverride`.
159
160
  | `label` | `string` (optional) | i18n key or literal replacing the item's label. |
160
161
  | `icon` | `ComponentType` (optional) | Icon component replacing the item's icon, from `@medusajs/icons`. |
161
162
  | `nested` | `NavParentId \| null` (optional) | Re-parent under a built-in parent id. `null` promotes a nested item to top level. |
163
+ | `group` | `NavGroupId` (optional) | List the item under a group declared in `groups`, or a built-in settings group. |
164
+
165
+ ### Groups
166
+
167
+ `groups` declares group headings by keyword, and an item's `group` assigns it to
168
+ one. A group renders in whichever sidebar its items live in.
169
+
170
+ The main sidebar has no built-in groups. Top-level items assigned to a declared
171
+ group render below the ungrouped list, under the group's heading. The settings
172
+ sidebar ships `general`, `developer`, and `myAccount` in the admin panel and
173
+ `general` in the vendor portal, which `groups` can also override by id. Settings
174
+ items are targeted as `settings/<segment>`.
175
+
176
+ ```ts apps/admin/src/_navigation.ts
177
+ import { defineNavigationConfig } from "@mercurjs/dashboard-sdk"
178
+
179
+ export default defineNavigationConfig({
180
+ groups: [
181
+ { id: "catalog", label: "Catalog" },
182
+ { id: "integrations", label: "Integrations", rank: 1 },
183
+ { id: "developer", hidden: true },
184
+ ],
185
+ items: [
186
+ { id: "products", group: "catalog" },
187
+ { id: "inventory", group: "catalog" },
188
+ { id: "settings/tax-regions", group: "integrations" },
189
+ ],
190
+ })
191
+ ```
192
+
193
+ Each entry is a `NavGroupConfig`.
194
+
195
+ | Field | Type | Description |
196
+ | --------------- | -------------------- | --------------------------------------------------------------------------- |
197
+ | `id` | `string` | A new keyword that declares a group, or a built-in settings group id to override. |
198
+ | `label` | `string` (optional) | Heading text. A literal, or an i18n key with `translationNs`. Defaults to the `id`. |
199
+ | `translationNs` | `string` (optional) | i18n namespace used to resolve `label`. |
200
+ | `rank` | `number` (optional) | Order among groups, lower first. Defaults to declaration order. Built-in settings groups rank `0`, `1`, `2`. |
201
+ | `hidden` | `boolean` (optional) | Remove the group and its items from the sidebar. |
202
+
203
+ A custom page joins a group through `config.group`. An undeclared group falls
204
+ back to the default placement (the ungrouped list, or the first settings group),
205
+ and a group with no visible items isn't rendered.
162
206
 
163
207
  Navigation overrides reshape existing items only. To add a new item, register a
164
208
  page with a `config.label` as shown above.
@@ -191,7 +191,11 @@ list: {
191
191
  { id: "brand_name", header: "Brand", component: ({ row }) => row.brand?.name }, // add from link
192
192
  ],
193
193
  bulkActions: [{ rank: 0, component: ArchiveBulkAction }],
194
- filters: [/* add or remove list filters */],
194
+ filters: [
195
+ { key: "customer_id", label: "Buyer email", type: "select", options: buyerEmails, searchable: true }, // replace a built-in filter
196
+ { key: "status", remove: true }, // remove a built-in filter
197
+ { key: "erp_synced", label: "Synced to ERP", type: "select", options: yesNo }, // add a new one
198
+ ],
195
199
  viewDefaults: {
196
200
  columnVisibility: { created_at: false },
197
201
  columnOrder: ["title", "sku", "erp_id"],
@@ -207,6 +211,21 @@ list: {
207
211
  | `header` | `string` (optional) | Column header text, for added columns. |
208
212
  | `component` | `ComponentType<{ row?, value? }>` (optional) | Cell renderer. Receives the full `row` and the cell `value`. |
209
213
 
214
+ `filters[]` is `CustomListFilter`. The `key` is the query param the filter
215
+ writes, so it matches a built-in filter by its param name (`customer_id`,
216
+ `status`, `created_at`, ...).
217
+
218
+ | Field | Type | Description |
219
+ | --------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
220
+ | `key` | `string` | Built-in filter key to replace or remove. Any other string adds a new filter. |
221
+ | `remove` | `true` (optional) | Drop the built-in filter with this `key`. No other fields are needed. |
222
+ | `label` | `string` | Label shown in the "Add filter" menu. |
223
+ | `type` | `"select" \| "string" \| "number" \| "date"` | Filter control. |
224
+ | `options` | `{ label: string; value: unknown }[]` | Choices for a `select` filter. `multiple` and `searchable` are also accepted. |
225
+
226
+ A replacement keeps the built-in filter's position; added filters go after the
227
+ built-in ones.
228
+
210
229
  <Note>
211
230
  **Bulk-action rendering is deferred in the MVP.** `bulkActions` are accepted and
212
231
  surfaced by the config, but not yet mounted into the list toolbar.
@@ -70,6 +70,7 @@ finds it at build time. There is no manifest to edit and no route to register.
70
70
  | ------ | -------------------------------- | ----------------------------------------------------------------------- |
71
71
  | `zone` | `WidgetZoneId \| WidgetZoneId[]` | The zone or zones to render in. Multiple widgets in one zone stack in registration order. |
72
72
  | `id` | `string` (optional) | A stable id. Derived from the file path at build time when omitted. |
73
+ | `label` | `string` (optional) | The tab label, as an i18n key or a literal. Only used by [tab zones](#tabs). |
73
74
 
74
75
  ## Component props
75
76
 
@@ -93,6 +94,89 @@ const ProductDetailNote = ({ data }: { data?: HttpTypes.AdminProduct }) => (
93
94
  authentication and receive no `data`.
94
95
  </Note>
95
96
 
97
+ ## Tabs
98
+
99
+ Some detail pages group their content into tabs. A tab zone lets a widget add a
100
+ tab of its own: the widget's `label` names the tab and its component renders as
101
+ the tab's panel. The placement decides whether the tab sits before or after the
102
+ built-in ones.
103
+
104
+ ```tsx apps/admin-test/src/widgets/store-loyalty-tab.tsx
105
+ import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
106
+ import { Container, Heading } from "@medusajs/ui"
107
+ import type { SellerDTO } from "@mercurjs/types"
108
+
109
+ const StoreLoyaltyTab = ({ data }: { data?: SellerDTO }) => (
110
+ <Container>
111
+ <Heading level="h2">Loyalty for {data?.name}</Heading>
112
+ </Container>
113
+ )
114
+
115
+ export const config = defineWidgetConfig({
116
+ zone: "stores.detail.tabs.after",
117
+ id: "loyalty",
118
+ label: "Loyalty",
119
+ })
120
+
121
+ export default StoreLoyaltyTab
122
+ ```
123
+
124
+ A widget that targets a tab zone without a `label` is not shown. `permission`
125
+ works as on any other widget and hides the tab from actors who lack it.
126
+
127
+ | Panel | Tab zone | Page |
128
+ | ----- | ------------------- | ------------------------------------------------------- |
129
+ | Admin | `stores.detail.tabs` | Store detail, next to Orders, Offers, Users and Time off. |
130
+
131
+ ## List item rows
132
+
133
+ The locations list renders one card per stock location. The `locations.list.item`
134
+ zone sits inside each card, so a widget there adds a row to the card instead of a
135
+ separate block above or below the list. `locations.list.item.before` renders
136
+ between the card header and the built-in rows, and `locations.list.item.after`
137
+ renders after them. `data` is the stock location of that card.
138
+
139
+ ```tsx apps/vendor/src/widgets/location-local-delivery-row.tsx
140
+ import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
141
+ import { StatusBadge, Text } from "@medusajs/ui"
142
+ import type { HttpTypes } from "@mercurjs/types"
143
+
144
+ type StockLocation = HttpTypes.VendorStockLocationResponse["stock_location"]
145
+
146
+ const LocationLocalDeliveryRow = ({ data }: { data?: StockLocation }) => {
147
+ const enabled = data?.fulfillment_sets?.some(
148
+ (set) => set.type === "local_delivery"
149
+ )
150
+
151
+ return (
152
+ <div className="flex items-center justify-between px-6 py-4">
153
+ <Text size="small" weight="plus" className="text-ui-fg-subtle flex-1">
154
+ Local delivery
155
+ </Text>
156
+ <div className="flex-1">
157
+ <StatusBadge color={enabled ? "green" : "grey"}>
158
+ {enabled ? "Enabled" : "Disabled"}
159
+ </StatusBadge>
160
+ </div>
161
+ </div>
162
+ )
163
+ }
164
+
165
+ export const config = defineWidgetConfig({
166
+ zone: "locations.list.item.after",
167
+ })
168
+
169
+ export default LocationLocalDeliveryRow
170
+ ```
171
+
172
+ Return a single row element, without a `Container` of its own. The card draws the
173
+ divider between rows.
174
+
175
+ | Panel | Zone | Page |
176
+ | ------ | --------------------- | -------------------------------------- |
177
+ | Vendor | `locations.list.item` | Settings → Locations, each location card. |
178
+ | Admin | `locations.list.item` | Settings → Locations, each location card. |
179
+
96
180
  ## Zone ids
97
181
 
98
182
  A zone id reads `<domain>.<view>.<placement>`.
@@ -120,7 +204,7 @@ expands to a `.before` and an `.after` zone.
120
204
  | `customer-groups` | ✓ | ✓ | | |
121
205
  | `customers` | ✓ | ✓ | ✓ | |
122
206
  | `inventory` | ✓ | ✓ | ✓ | |
123
- | `locations` | ✓ | ✓ | ✓ | |
207
+ | `locations` | ✓ | ✓ | ✓ | `list.item` |
124
208
  | `offer-variants` | | ✓ | ✓ | |
125
209
  | `offers` | ✓ | ✓ | ✓ | |
126
210
  | `orders` | ✓ | ✓ | ✓ | `detail.summary` |
@@ -167,7 +251,7 @@ cell expands to a `.before` and an `.after` zone.
167
251
  | `customer-groups` | ✓ | ✓ | | |
168
252
  | `customers` | ✓ | ✓ | ✓ | |
169
253
  | `inventory` | ✓ | ✓ | ✓ | |
170
- | `locations` | ✓ | ✓ | ✓ | |
254
+ | `locations` | ✓ | ✓ | ✓ | `list.item` |
171
255
  | `marketplace` | | ✓ | | |
172
256
  | `offer-variants` | | ✓ | ✓ | |
173
257
  | `offers` | ✓ | ✓ | ✓ | |
@@ -151,6 +151,7 @@ export default defineNavigationConfig({
151
151
 
152
152
  - `id` targets any built-in item, top-level routes or nested children, by its stable id, typed as `NavItemId`.
153
153
  - `rank` orders an item within its parent, `hidden` removes it from the sidebar, `label` and `icon` relabel it, and `nested` re-parents it (`nested: null` promotes to top level, typed against `NavParentId`).
154
+ - `groups` declares group headings by keyword, and an item's `group` lists it under one, in the main sidebar or the settings sidebar (settings items are targeted as `settings/<segment>`). The built-in settings groups can be reshaped by id. A custom page picks its group with `group` in its route config.
154
155
  - 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`.
155
156
 
156
157
  Walk through it in [Customize navigation](/rc/resources/tutorials/customize-navigation).
@@ -242,6 +243,7 @@ For the full walkthrough (forms, sections, and list columns), see [Extend forms
242
243
  | `icon` | `ComponentType` | Icon component (for example from `@medusajs/icons`) |
243
244
  | `rank` | `number` | Sort order (lower numbers appear first) |
244
245
  | `nested` | `string` | Parent path for nested menu items |
246
+ | `group` | `string` | Sidebar group declared in `_navigation.ts` to list the item under |
245
247
  | `translationNs` | `string` | i18n namespace for the label |
246
248
  | `public` | `boolean` | If `true`, the route is accessible without authentication |
247
249
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "How to Customize Navigation"
3
3
  sidebarTitle: "Customize Navigation"
4
- description: "Reorder, hide, relabel, and re-parent the panel's built-in sidebar items from a single _navigation.ts file."
4
+ description: "Reorder, hide, relabel, re-parent, and group the panel's built-in sidebar items from a single _navigation.ts file."
5
5
  ---
6
6
 
7
7
  Reshape the built-in sidebar without replacing it. You author one host-owned file, `src/_navigation.ts`, and it reorders, hides, relabels, and re-parents the built-in items.
@@ -55,8 +55,9 @@ With it present, `id` and `nested` autocomplete and an unknown id fails `tsc`.
55
55
  | `label` | `string` | Relabel with an i18n key or literal. |
56
56
  | `icon` | `ComponentType` | Replace its icon. |
57
57
  | `nested` | `NavParentId \| null` | Re-parent under another top-level item. `null` promotes a nested item to top level. |
58
+ | `group` | `NavGroupId` | List the item under a [group heading](#group-items-under-headings). |
58
59
 
59
- Both `id` and `nested` are checked against the panel's generated `NavItemRegistry` and `NavParentRegistry`.
60
+ `id`, `nested`, and `group` are checked against the panel's generated `NavItemRegistry`, `NavParentRegistry`, and `NavGroupRegistry`.
60
61
  </Step>
61
62
  <Step title="Reload the panel">
62
63
  Open the vendor portal. Orders sits at the top, Price Lists is gone from the menu, and Campaigns now appears under Orders.
@@ -80,6 +81,80 @@ export default defineNavigationConfig({
80
81
  })
81
82
  ```
82
83
 
84
+ ## Group items under headings
85
+
86
+ Declare groups by keyword in `groups`, then assign items to them with `group`. A group renders in whichever sidebar its items live in, and a group with no visible items isn't rendered.
87
+
88
+ ### Main sidebar
89
+
90
+ The main sidebar ships without groups. Items you assign to a declared group move out of the default list and render below it, under the group's heading.
91
+
92
+ ```ts apps/vendor/src/_navigation.ts
93
+ import { defineNavigationConfig } from "@mercurjs/dashboard-sdk"
94
+
95
+ export default defineNavigationConfig({
96
+ groups: [
97
+ { id: "catalog", label: "Catalog" },
98
+ { id: "finance", label: "Finance" },
99
+ ],
100
+ items: [
101
+ { id: "products", group: "catalog" },
102
+ { id: "inventory", group: "catalog" },
103
+ { id: "payouts", group: "finance" },
104
+ ],
105
+ })
106
+ ```
107
+
108
+ Only top-level items can be grouped. Nested children follow their parent.
109
+
110
+ ### Settings sidebar
111
+
112
+ The settings sidebar already lists its items under groups. The admin panel ships `general`, `developer`, and `myAccount`. The vendor portal ships `general`. You can reshape these built-in groups, add your own, and move items between them. Settings items are targeted by their path without the leading slash, such as `settings/users`.
113
+
114
+ ```ts apps/admin/src/_navigation.ts
115
+ import { defineNavigationConfig } from "@mercurjs/dashboard-sdk"
116
+
117
+ export default defineNavigationConfig({
118
+ groups: [
119
+ { id: "integrations", label: "Integrations", rank: 1 }, // new group, after General
120
+ { id: "developer", label: "API" }, // relabel a built-in group
121
+ ],
122
+ items: [
123
+ { id: "settings/secret-api-keys", group: "integrations" },
124
+ { id: "settings/tax-regions", group: "integrations", rank: 0 },
125
+ ],
126
+ })
127
+ ```
128
+
129
+ ### Group fields
130
+
131
+ | Field | Type | Effect |
132
+ |-------|------|--------|
133
+ | `id` | `string` | **Required.** A new keyword to declare a group, or a built-in settings group id to override. |
134
+ | `label` | `string` | Heading text. A literal, or an i18n key when `translationNs` is set. Defaults to the `id`. |
135
+ | `translationNs` | `string` | i18n namespace used to resolve `label`. |
136
+ | `rank` | `number` | Order among groups, lower first. Defaults to declaration order. Built-in settings groups rank `0`, `1`, `2`. |
137
+ | `hidden` | `boolean` | Remove the group and its items from the sidebar. |
138
+
139
+ An item's `group` only accepts a built-in group id or an id declared in the same file's `groups`, so a typo fails `tsc`.
140
+
141
+ ### Group a custom page
142
+
143
+ A custom page picks its group in its own route config with the same keyword.
144
+
145
+ ```tsx apps/admin/src/routes/settings/webhooks/page.tsx
146
+ import type { RouteConfig } from "@mercurjs/dashboard-sdk"
147
+
148
+ export const config: RouteConfig = {
149
+ label: "Webhooks",
150
+ group: "integrations",
151
+ }
152
+ ```
153
+
154
+ <Note>
155
+ A page whose group isn't declared falls back to the default placement: the ungrouped list in the main sidebar, or the first group in the settings sidebar.
156
+ </Note>
157
+
83
158
  ## Verify
84
159
 
85
160
  1. The top-level order reflects your `rank` values, with `orders` first.
@@ -94,6 +169,9 @@ export default defineNavigationConfig({
94
169
  <Accordion title="Can an installed block reorder the sidebar?">
95
170
  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`.
96
171
  </Accordion>
172
+ <Accordion title="Can a block put its page in a group?">
173
+ Yes. A block's page sets `group` in its route config like any other page. If the host hasn't declared that group, the page falls back to the default placement.
174
+ </Accordion>
97
175
  <Accordion title="What ids can I target?">
98
176
  Any built-in item, top-level or nested, by its own id, such as `orders`, `products`, `categories`, `collections`, `campaigns`, or `customer-groups`. Let your editor autocomplete `id:` against `NavItemId`. The full set is generated into your panel's `extension-targets.d.ts`.
99
177
  </Accordion>
package/llms.txt CHANGED
@@ -51,7 +51,7 @@ package (`node_modules/@mercurjs/docs/`).
51
51
  - [How to Build a Block](content/resources/tutorials/build-a-block.mdx) — Author a reusable feature as a block covering backend, panel UI, and docs, then build it into a registry and install it into any Mercur project.
52
52
  - [How to Add a Custom API Route](content/resources/tutorials/custom-api-route.mdx) — Add a backend endpoint, regenerate the route map, and call it from a panel page with full type safety.
53
53
  - [How to Create a Custom Page](content/resources/tutorials/custom-panel-page.mdx) — Add a page to the vendor portal or admin panel with file-based routing and let the dashboard SDK wire it into the sidebar.
54
- - [How to Customize Navigation](content/resources/tutorials/customize-navigation.mdx) — Reorder, hide, relabel, and re-parent the panel's built-in sidebar items from a single _navigation.ts file.
54
+ - [How to Customize Navigation](content/resources/tutorials/customize-navigation.mdx) — Reorder, hide, relabel, re-parent, and group the panel's built-in sidebar items from a single _navigation.ts file.
55
55
  - [How to Extend Forms and Tables](content/resources/tutorials/extend-forms-and-tables.mdx) — Add validated fields, detail-section displays, and list columns to a built-in model from a single file with defineCustomFieldsConfig.
56
56
  - [How to Extend Onboarding](content/resources/tutorials/extend-onboarding.mdx) — Add a field to the vendor store-setup surface, carry it through additional_data, and persist it from a workflow hook without forking core.
57
57
  - [How to Create Master Products and Offers](content/resources/tutorials/master-products-and-offers.mdx) — Publish one master product, add two competing seller offers, and read per-offer prices from the Store API.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercurjs/docs",
3
- "version": "2.3.6-canary.8",
3
+ "version": "2.3.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/mercurjs/mercur",