Ver Fonte

feat(dashboard): Improved extensibility of ActionBar (#4049)

Michael Bromley há 1 mês atrás
pai
commit
f6184b6335
62 ficheiros alterados com 1013 adições e 660 exclusões
  1. 9 12
      docs/docs/guides/extending-the-dashboard/creating-pages/detail-pages.md
  2. 3 3
      docs/docs/guides/extending-the-dashboard/creating-pages/list-pages.md
  3. 35 2
      docs/docs/guides/extending-the-dashboard/customizing-pages/action-bar-items.md
  4. 9 12
      packages/dashboard/src/app/routes/_authenticated/_administrators/administrators.tsx
  5. 9 12
      packages/dashboard/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx
  6. 6 9
      packages/dashboard/src/app/routes/_authenticated/_assets/assets_.$id.tsx
  7. 9 12
      packages/dashboard/src/app/routes/_authenticated/_channels/channels.tsx
  8. 9 12
      packages/dashboard/src/app/routes/_authenticated/_channels/channels_.$id.tsx
  9. 9 12
      packages/dashboard/src/app/routes/_authenticated/_collections/collections.tsx
  10. 9 12
      packages/dashboard/src/app/routes/_authenticated/_collections/collections_.$id.tsx
  11. 9 12
      packages/dashboard/src/app/routes/_authenticated/_countries/countries.tsx
  12. 9 12
      packages/dashboard/src/app/routes/_authenticated/_countries/countries_.$id.tsx
  13. 9 12
      packages/dashboard/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx
  14. 9 12
      packages/dashboard/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx
  15. 9 12
      packages/dashboard/src/app/routes/_authenticated/_customers/customers.tsx
  16. 9 12
      packages/dashboard/src/app/routes/_authenticated/_customers/customers_.$id.tsx
  17. 9 12
      packages/dashboard/src/app/routes/_authenticated/_facets/facets.tsx
  18. 9 12
      packages/dashboard/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx
  19. 9 12
      packages/dashboard/src/app/routes/_authenticated/_facets/facets_.$id.tsx
  20. 9 12
      packages/dashboard/src/app/routes/_authenticated/_global-settings/global-settings.tsx
  21. 37 40
      packages/dashboard/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx
  22. 3 3
      packages/dashboard/src/app/routes/_authenticated/_orders/orders.tsx
  23. 3 3
      packages/dashboard/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx
  24. 27 30
      packages/dashboard/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx
  25. 9 12
      packages/dashboard/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx
  26. 9 12
      packages/dashboard/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx
  27. 9 12
      packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx
  28. 15 18
      packages/dashboard/src/app/routes/_authenticated/_products/products.tsx
  29. 9 12
      packages/dashboard/src/app/routes/_authenticated/_products/products_.$id.tsx
  30. 9 12
      packages/dashboard/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx
  31. 9 12
      packages/dashboard/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx
  32. 3 3
      packages/dashboard/src/app/routes/_authenticated/_profile/profile.tsx
  33. 9 12
      packages/dashboard/src/app/routes/_authenticated/_promotions/promotions.tsx
  34. 9 12
      packages/dashboard/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx
  35. 9 12
      packages/dashboard/src/app/routes/_authenticated/_roles/roles.tsx
  36. 9 12
      packages/dashboard/src/app/routes/_authenticated/_roles/roles_.$id.tsx
  37. 9 12
      packages/dashboard/src/app/routes/_authenticated/_sellers/sellers.tsx
  38. 9 12
      packages/dashboard/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx
  39. 11 12
      packages/dashboard/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx
  40. 19 20
      packages/dashboard/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx
  41. 9 12
      packages/dashboard/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx
  42. 9 12
      packages/dashboard/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx
  43. 3 3
      packages/dashboard/src/app/routes/_authenticated/_system/job-queue.tsx
  44. 9 12
      packages/dashboard/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx
  45. 9 12
      packages/dashboard/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx
  46. 9 12
      packages/dashboard/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx
  47. 9 12
      packages/dashboard/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx
  48. 9 12
      packages/dashboard/src/app/routes/_authenticated/_zones/zones.tsx
  49. 9 12
      packages/dashboard/src/app/routes/_authenticated/_zones/zones_.$id.tsx
  50. 5 3
      packages/dashboard/src/app/routes/_authenticated/index.tsx
  51. 1 1
      packages/dashboard/src/lib/components/data-table/column-header-wrapper.tsx
  52. 7 3
      packages/dashboard/src/lib/components/layout/dev-mode-indicator.tsx
  53. 1 1
      packages/dashboard/src/lib/components/layout/nav-item-wrapper.tsx
  54. 8 3
      packages/dashboard/src/lib/components/shared/asset/asset-gallery.tsx
  55. 33 0
      packages/dashboard/src/lib/framework/extension-api/types/layout.ts
  56. 185 0
      packages/dashboard/src/lib/framework/layout-engine/action-bar-item-wrapper.tsx
  57. 15 13
      packages/dashboard/src/lib/framework/layout-engine/dev-mode-button.tsx
  58. 3 1
      packages/dashboard/src/lib/framework/layout-engine/location-wrapper.tsx
  59. 237 41
      packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx
  60. 1 0
      packages/dashboard/src/lib/index.ts
  61. 7 2
      packages/dev-server/test-plugins/reviews/dashboard/index.tsx
  62. 12 2
      packages/dev-server/test-plugins/reviews/dashboard/review-list.tsx

+ 9 - 12
docs/docs/guides/extending-the-dashboard/creating-pages/detail-pages.md

@@ -154,8 +154,6 @@ import {
     Page,
     PageTitle,
     PageActionBar,
-    PageActionBarRight,
-    PermissionGuard,
     Button,
     PageLayout,
     PageBlock,
@@ -165,6 +163,7 @@ import {
     Input,
     RichTextInput,
     CustomFieldsPageBlock,
+    ActionBarItem,
 } from '@vendure/dashboard';
 import { AnyRoute, useNavigate } from '@tanstack/react-router';
 import { toast } from 'sonner';
@@ -253,16 +252,14 @@ function ArticleDetailPage({ route }: { route: AnyRoute }) {
         <Page pageId="article-detail" form={form} submitHandler={submitHandler}>
             <PageTitle>{creatingNewEntity ? 'New article' : (entity?.title ?? '')}</PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateProduct', 'UpdateCatalog']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            Update
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem requires={['UpdateProduct', 'UpdateCatalog']} itemId="save-button">
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        Update
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="side" blockId="publish-status">

+ 3 - 3
docs/docs/guides/extending-the-dashboard/creating-pages/list-pages.md

@@ -35,7 +35,7 @@ import {
     Button,
     DashboardRouteDefinition,
     ListPage,
-    PageActionBarRight,
+    ActionBarItem,
     DetailPageButton,
 } from '@vendure/dashboard';
 import { Link } from '@tanstack/react-router';
@@ -101,14 +101,14 @@ export const articleList: DashboardRouteDefinition = {
                 },
             }}
         >
-            <PageActionBarRight>
+            <ActionBarItem>
                 <Button asChild>
                     <Link to="./new">
                         <PlusIcon className="mr-2 h-4 w-4" />
                         New article
                     </Link>
                 </Button>
-            </PageActionBarRight>
+            </ActionBarItem>
         </ListPage>
     ),
 };

+ 35 - 2
docs/docs/guides/extending-the-dashboard/customizing-pages/action-bar-items.md

@@ -36,6 +36,39 @@ defineDashboardExtension({
 
 ![Action bar button](action-bar-button.webp)
 
+## Location and position
+
+The `pageId` property is required in order to specify on which page the action bar item should appear.
+By default, the item will be placed to the left of any existing action bar items.
+
+However, since Vendure v3.6.0, you can also specify the `position` property for more control over the placement of your action bar item.
+
+See the section on [finding page and item ids](#finding-page-ids) below for help in determining the correct `pageId` and `itemId` values.
+
+```typescript
+import { Button, defineDashboardExtension } from '@vendure/dashboard';
+import { useState } from 'react';
+
+defineDashboardExtension({
+    actionBarItems: [
+        {
+            pageId: 'product-detail',
+            component: ({ context }) => {
+                // omitted for brevity
+            },
+            // highlight-start
+            position: {
+                // The ID of the existing action bar item to position relative to
+                itemId: 'save-button',
+                // Order can be 'before', 'after' or 'replace'
+                order: 'after',
+            },
+            // highlight-end
+        },
+    ],
+});
+```
+
 ## Context Data
 
 The `context` prop provides access to:
@@ -262,9 +295,9 @@ The dashboard provides several button variants you can use:
 6. **Group related actions**: Consider the order and grouping of multiple action items
 7. **Test thoroughly**: Verify your actions work correctly across different entity states
 
-## Finding Page IDs
+## Finding Page and Item IDs
 
-To find the `pageId` for your action bar items:
+To find the `pageId` and `itemId` for your action bar items:
 
 1. Enable [Dev Mode](/guides/extending-the-dashboard/extending-overview/#dev-mode) in the dashboard
 2. Navigate to the page where you want to add your action

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_administrators/administrators.tsx

@@ -1,9 +1,8 @@
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { RoleCodeLabel } from '@/vdb/components/shared/role-code-label.js';
 import { Badge } from '@/vdb/components/ui/badge.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -89,16 +88,14 @@ function AdministratorListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreateAdministrator']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon />
-                            New Administrator
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateAdministrator']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon />
+                        New Administrator
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx

@@ -1,15 +1,14 @@
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { RoleSelector } from '@/vdb/components/shared/role-selector.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -99,16 +98,14 @@ function AdministratorDetailPage() {
             <PageTitle>{creatingNewEntity ? <Trans>New administrator</Trans> : name}</PageTitle>
 
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateAdministrator']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateAdministrator']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 6 - 9
packages/dashboard/src/app/routes/_authenticated/_assets/assets_.$id.tsx

@@ -3,15 +3,14 @@ import { AssetPreviewSelector } from '@/vdb/components/shared/asset/asset-previe
 import { PreviewPreset } from '@/vdb/components/shared/asset/asset-preview.js';
 import { AssetProperties } from '@/vdb/components/shared/asset/asset-properties.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { VendureImage } from '@/vdb/components/shared/vendure-image.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Label } from '@/vdb/components/ui/label.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -95,13 +94,11 @@ function AssetDetailPage() {
                 <Trans>Edit asset</Trans>
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateChannel']}>
-                        <Button type="submit" disabled={!form.formState.isDirty || isPending}>
-                            <Trans>Update</Trans>
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateChannel']}>
+                    <Button type="submit" disabled={!form.formState.isDirty || isPending}>
+                        <Trans>Update</Trans>
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="asset-preview">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_channels/channels.tsx

@@ -1,8 +1,7 @@
 import { ChannelCodeLabel } from '@/vdb/components/shared/channel-code-label.js';
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
 import { Trans } from '@lingui/react/macro';
@@ -66,16 +65,14 @@ function ChannelListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreateChannel']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon className="mr-2 h-4 w-4" />
-                            <Trans>New Channel</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateChannel']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon className="mr-2 h-4 w-4" />
+                        <Trans>New Channel</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_channels/channels_.$id.tsx

@@ -3,7 +3,6 @@ import { CurrencySelector } from '@/vdb/components/shared/currency-selector.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
 import { LanguageSelector } from '@/vdb/components/shared/language-selector.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { SellerSelector } from '@/vdb/components/shared/seller-selector.js';
 import { ZoneSelector } from '@/vdb/components/shared/zone-selector.js';
 import { Button } from '@/vdb/components/ui/button.js';
@@ -11,11 +10,11 @@ import { Input } from '@/vdb/components/ui/input.js';
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { DEFAULT_CHANNEL_CODE, NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -116,16 +115,14 @@ function ChannelDetailPage() {
                 )}
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateChannel']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateChannel']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_collections/collections.tsx

@@ -1,7 +1,6 @@
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { api } from '@/vdb/graphql/api.js';
 import { Trans } from '@lingui/react/macro';
@@ -239,16 +238,14 @@ function CollectionListPage() {
                     },
                 ]}
             >
-                <PageActionBarRight>
-                    <PermissionGuard requires={['CreateCollection', 'CreateCatalog']}>
-                        <Button asChild>
-                            <Link to="./new">
-                                <PlusIcon className="mr-2 h-4 w-4" />
-                                <Trans>New Collection</Trans>
-                            </Link>
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="create-button" requiresPermission={['CreateCollection', 'CreateCatalog']}>
+                    <Button asChild>
+                        <Link to="./new">
+                            <PlusIcon className="mr-2 h-4 w-4" />
+                            <Trans>New Collection</Trans>
+                        </Link>
+                    </Button>
+                </ActionBarItem>
             </ListPage>
         </>
     );

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_collections/collections_.$id.tsx

@@ -3,7 +3,6 @@ import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
 import { EntityAssets } from '@/vdb/components/shared/entity-assets.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '@/vdb/components/ui/form.js';
@@ -11,11 +10,11 @@ import { Input } from '@/vdb/components/ui/input.js';
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -115,16 +114,14 @@ function CollectionDetailPage() {
         <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
             <PageTitle>{creatingNewEntity ? <Trans>New collection</Trans> : (entity?.name ?? '')}</PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateCollection', 'UpdateCatalog']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateCollection', 'UpdateCatalog']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="side" blockId="privacy">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_countries/countries.tsx

@@ -1,7 +1,6 @@
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -57,16 +56,14 @@ function CountryListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreateCountry']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon />
-                            <Trans>Add Country</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateCountry']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon />
+                        <Trans>Add Country</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_countries/countries_.$id.tsx

@@ -1,17 +1,16 @@
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -78,16 +77,14 @@ function CountryDetailPage() {
         <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
             <PageTitle>{creatingNewEntity ? <Trans>New country</Trans> : (entity?.name ?? '')}</PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateCountry']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateCountry']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="side" blockId="enabled">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx

@@ -1,7 +1,6 @@
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -57,16 +56,14 @@ function CustomerGroupListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreateCustomerGroup']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon className="mr-2 h-4 w-4" />
-                            <Trans>New Customer Group</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateCustomerGroup']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon className="mr-2 h-4 w-4" />
+                        <Trans>New Customer Group</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx

@@ -1,15 +1,14 @@
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -87,16 +86,14 @@ function CustomerGroupDetailPage() {
                 {creatingNewEntity ? <Trans>New customer group</Trans> : (entity?.name ?? '')}
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateCustomerGroup']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateCustomerGroup']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_customers/customers.tsx

@@ -1,8 +1,7 @@
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Badge } from '@/vdb/components/ui/badge.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -89,16 +88,14 @@ function CustomerListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreateCustomer']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon />
-                            <Trans>New Customer</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateCustomer']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon />
+                        <Trans>New Customer</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_customers/customers_.$id.tsx

@@ -2,7 +2,6 @@ import { CustomerGroupChip } from '@/vdb/components/shared/customer-group-chip.j
 import { CustomerGroupSelector } from '@/vdb/components/shared/customer-group-selector.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import {
     Dialog,
@@ -15,11 +14,11 @@ import {
 import { Input } from '@/vdb/components/ui/input.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -146,16 +145,14 @@ function CustomerDetailPage() {
         <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
             <PageTitle>{creatingNewEntity ? <Trans>New customer</Trans> : customerName}</PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateCustomer']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateCustomer']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_facets/facets.tsx

@@ -1,10 +1,9 @@
 import { DataTableCellComponent } from '@/vdb/components/data-table/types.js';
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
 import { FacetValueChip } from '@/vdb/components/shared/facet-value-chip.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Badge } from '@/vdb/components/ui/badge.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -122,16 +121,14 @@ function FacetListPage() {
             ]}
             route={Route}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreateFacet', 'CreateCatalog']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon className="mr-2 h-4 w-4" />
-                            <Trans>New Facet</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateFacet', 'CreateCatalog']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon className="mr-2 h-4 w-4" />
+                        <Trans>New Facet</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx

@@ -2,17 +2,16 @@ import { SlugInput } from '@/vdb/components/data-input/index.js';
 import { PageBreadcrumb } from '@/vdb/components/layout/generated-breadcrumbs.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -104,16 +103,14 @@ function FacetValueDetailPage() {
                 {creatingNewEntity ? <Trans>New facet value</Trans> : ((entity as any)?.name ?? '')}
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateProduct', 'UpdateCatalog']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateProduct', 'UpdateCatalog']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 {entity?.facet && (

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_facets/facets_.$id.tsx

@@ -1,18 +1,17 @@
 import { SlugInput } from '@/vdb/components/data-input/index.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -93,16 +92,14 @@ function FacetDetailPage() {
         <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
             <PageTitle>{creatingNewEntity ? <Trans>New facet</Trans> : (entity?.name ?? '')}</PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateProduct', 'UpdateCatalog']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateProduct', 'UpdateCatalog']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="side" blockId="privacy">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_global-settings/global-settings.tsx

@@ -2,18 +2,17 @@ import { NumberInput } from '@/vdb/components/data-input/number-input.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
 import { LanguageSelector } from '@/vdb/components/shared/language-selector.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import { extendDetailFormQuery } from '@/vdb/framework/document-extension/extend-detail-form-query.js';
 import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -92,16 +91,14 @@ function GlobalSettingsPage() {
                 <Trans>Global Settings</Trans>
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateSettings', 'UpdateGlobalSettings']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            <Trans>Update</Trans>
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateSettings', 'UpdateGlobalSettings']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        <Trans>Update</Trans>
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 37 - 40
packages/dashboard/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx

@@ -1,12 +1,11 @@
 import { CustomFieldsForm } from '@/vdb/components/shared/custom-fields-form.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { DropdownMenuItem } from '@/vdb/components/ui/dropdown-menu.js';
 import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
 import {
+    ActionBarItem,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -157,44 +156,42 @@ export function OrderDetailShared({
     return (
         <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
             <PageTitle>{titleSlot?.(entity) || <DefaultOrderTitle entity={entity} />}</PageTitle>
-            <PageActionBar>
-                <PageActionBarRight
-                    dropdownMenuItems={[
-                        ...(nextStates.includes('Modifying')
-                            ? [
-                                  {
-                                      component: () => (
-                                          <DropdownMenuItem onClick={handleModifyClick}>
-                                              <Pencil className="w-4 h-4" />
-                                              <Trans>Modify</Trans>
-                                          </DropdownMenuItem>
-                                      ),
-                                  },
-                              ]
-                            : []),
-                    ]}
-                >
-                    {showAddPaymentButton && (
-                        <PermissionGuard requires={['UpdateOrder']}>
-                            <AddManualPaymentDialog
-                                order={entity}
-                                onSuccess={() => {
-                                    refreshEntity();
-                                }}
-                            />
-                        </PermissionGuard>
-                    )}
-                    {showFulfillButton && (
-                        <PermissionGuard requires={['UpdateOrder']}>
-                            <FulfillOrderDialog
-                                order={entity}
-                                onSuccess={() => {
-                                    refreshOrderAndHistory();
-                                }}
-                            />
-                        </PermissionGuard>
-                    )}
-                </PageActionBarRight>
+            <PageActionBar
+                dropdownMenuItems={[
+                    ...(nextStates.includes('Modifying')
+                        ? [
+                              {
+                                  component: () => (
+                                      <DropdownMenuItem onClick={handleModifyClick}>
+                                          <Pencil className="w-4 h-4" />
+                                          <Trans>Modify</Trans>
+                                      </DropdownMenuItem>
+                                  ),
+                              },
+                          ]
+                        : []),
+                ]}
+            >
+                {showAddPaymentButton && (
+                    <ActionBarItem itemId="add-payment-button" requiresPermission={['UpdateOrder']}>
+                        <AddManualPaymentDialog
+                            order={entity}
+                            onSuccess={() => {
+                                refreshEntity();
+                            }}
+                        />
+                    </ActionBarItem>
+                )}
+                {showFulfillButton && (
+                    <ActionBarItem itemId="fulfill-order-button" requiresPermission={['UpdateOrder']}>
+                        <FulfillOrderDialog
+                            order={entity}
+                            onSuccess={() => {
+                                refreshOrderAndHistory();
+                            }}
+                        />
+                    </ActionBarItem>
+                )}
             </PageActionBar>
             <PageLayout>
                 {/* Main Column Blocks */}

+ 3 - 3
packages/dashboard/src/app/routes/_authenticated/_orders/orders.tsx

@@ -5,7 +5,7 @@ import {
     OrderStateCell,
 } from '@/vdb/components/shared/table-cell/order-table-cell-components.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { api } from '@/vdb/graphql/api.js';
 import { ResultOf } from '@/vdb/graphql/graphql.js';
@@ -109,12 +109,12 @@ function OrderListPage() {
                 },
             }}
         >
-            <PageActionBarRight>
+            <ActionBarItem itemId="create-draft-button">
                 <Button onClick={() => createDraftOrder({})}>
                     <PlusIcon className="mr-2 h-4 w-4" />
                     <Trans>Draft order</Trans>
                 </Button>
-            </PageActionBarRight>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 3 - 3
packages/dashboard/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx

@@ -1,9 +1,9 @@
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import {
+    ActionBarItem,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -159,11 +159,11 @@ function ModifyOrderPage() {
                 <Trans>Modify order</Trans>
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
+                <ActionBarItem itemId="cancel-modification-button">
                     <Button type="button" variant="secondary" onClick={handleCancelModificationClick}>
                         <Trans>Cancel modification</Trans>
                     </Button>
-                </PageActionBarRight>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="order-lines" title={<Trans>Order lines</Trans>}>

+ 27 - 30
packages/dashboard/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx

@@ -2,15 +2,14 @@ import { ConfirmationDialog } from '@/vdb/components/shared/confirmation-dialog.
 import { CustomFieldsForm } from '@/vdb/components/shared/custom-fields-form.js';
 import { CustomerSelector } from '@/vdb/components/shared/customer-selector.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Form } from '@/vdb/components/ui/form.js';
 import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
 import { useGeneratedForm } from '@/vdb/framework/form-engine/use-generated-form.js';
 import {
+    ActionBarItem,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -295,35 +294,33 @@ function DraftOrderPage() {
                 <Trans>Draft order</Trans>: {entity?.code ?? ''}
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['DeleteOrder']}>
-                        <ConfirmationDialog
-                            title={t`Delete draft order`}
-                            description={t`Are you sure you want to delete this draft order?`}
-                            onConfirm={() => {
-                                deleteDraftOrder({ orderId: entity.id });
-                            }}
-                        >
-                            <Button variant="destructive" type="button">
-                                <Trans>Delete draft</Trans>
-                            </Button>
-                        </ConfirmationDialog>
-                    </PermissionGuard>
-                    <PermissionGuard requires={['UpdateOrder']}>
-                        <Button
-                            type="button"
-                            disabled={
-                                !entity.customer ||
-                                entity.lines.length === 0 ||
-                                entity.shippingLines.length === 0 ||
-                                entity.state !== 'Draft'
-                            }
-                            onClick={() => completeDraftOrder({ id: entity.id, state: 'ArrangingPayment' })}
-                        >
-                            <Trans>Complete draft</Trans>
+                <ActionBarItem itemId="delete-button" requiresPermission={['DeleteOrder']}>
+                    <ConfirmationDialog
+                        title={t`Delete draft order`}
+                        description={t`Are you sure you want to delete this draft order?`}
+                        onConfirm={() => {
+                            deleteDraftOrder({ orderId: entity.id });
+                        }}
+                    >
+                        <Button variant="destructive" type="button">
+                            <Trans>Delete draft</Trans>
                         </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                    </ConfirmationDialog>
+                </ActionBarItem>
+                <ActionBarItem itemId="complete-draft-button" requiresPermission={['UpdateOrder']}>
+                    <Button
+                        type="button"
+                        disabled={
+                            !entity.customer ||
+                            entity.lines.length === 0 ||
+                            entity.shippingLines.length === 0 ||
+                            entity.state !== 'Draft'
+                        }
+                        onClick={() => completeDraftOrder({ id: entity.id, state: 'ArrangingPayment' })}
+                    >
+                        <Trans>Complete draft</Trans>
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="order-table">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx

@@ -1,9 +1,8 @@
 import { BooleanDisplayBadge } from '@/vdb/components/data-display/boolean.js';
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { RichTextDescriptionCell } from '@/vdb/components/shared/table-cell/order-table-cell-components.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans, useLingui } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -73,16 +72,14 @@ function PaymentMethodListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreatePaymentMethod']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon className="mr-2 h-4 w-4" />
-                            <Trans>New Payment Method</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreatePaymentMethod']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon className="mr-2 h-4 w-4" />
+                        <Trans>New Payment Method</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx

@@ -1,18 +1,17 @@
 import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -121,16 +120,14 @@ function PaymentMethodDetailPage() {
                 {creatingNewEntity ? <Trans>New payment method</Trans> : (entity?.name ?? '')}
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdatePaymentMethod']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdatePaymentMethod']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="side" blockId="enabled">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx

@@ -5,7 +5,6 @@ import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js'
 import { EntityAssets } from '@/vdb/components/shared/entity-assets.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { TaxCategorySelector } from '@/vdb/components/shared/tax-category-selector.js';
 import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
 import { Button } from '@/vdb/components/ui/button.js';
@@ -15,11 +14,11 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -208,16 +207,14 @@ function ProductVariantDetailPage() {
                 {creatingNewEntity ? <Trans>New product variant</Trans> : (entity?.name ?? '')}
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateProduct', 'UpdateCatalog']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateProduct', 'UpdateCatalog']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="side" blockId="enabled">

+ 15 - 18
packages/dashboard/src/app/routes/_authenticated/_products/products.tsx

@@ -1,8 +1,7 @@
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { RichTextDescriptionCell } from '@/vdb/components/shared/table-cell/order-table-cell-components.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { api } from '@/vdb/graphql/api.js';
 import { Trans, useLingui } from '@lingui/react/macro';
@@ -100,22 +99,20 @@ function ProductListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['UpdateCatalog']}>
-                    <Button variant="outline" onClick={handleRebuildSearchIndex}>
-                        <ListRestart />
-                        <Trans>Rebuild search index</Trans>
-                    </Button>
-                </PermissionGuard>
-                <PermissionGuard requires={['CreateProduct', 'CreateCatalog']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon className="mr-2 h-4 w-4" />
-                            <Trans>New Product</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="rebuild-index-button" requiresPermission={['UpdateCatalog']}>
+                <Button variant="outline" onClick={handleRebuildSearchIndex}>
+                    <ListRestart />
+                    <Trans>Rebuild search index</Trans>
+                </Button>
+            </ActionBarItem>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateProduct', 'CreateCatalog']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon className="mr-2 h-4 w-4" />
+                        <Trans>New Product</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_products/products_.$id.tsx

@@ -4,7 +4,6 @@ import { AssignedFacetValues } from '@/vdb/components/shared/assigned-facet-valu
 import { EntityAssets } from '@/vdb/components/shared/entity-assets.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { FormControl, FormDescription, FormItem, FormMessage } from '@/vdb/components/ui/form.js';
@@ -12,11 +11,11 @@ import { Input } from '@/vdb/components/ui/input.js';
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -102,16 +101,14 @@ function ProductDetailPage() {
         <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
             <PageTitle>{creatingNewEntity ? <Trans>New product</Trans> : (entity?.name ?? '')}</PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateProduct', 'UpdateCatalog']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateProduct', 'UpdateCatalog']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="side" blockId="enabled-toggle">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx

@@ -1,7 +1,6 @@
 import { SlugInput } from '@/vdb/components/data-input/index.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
@@ -9,11 +8,11 @@ import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import { extendDetailFormQuery } from '@/vdb/framework/document-extension/extend-detail-form-query.js';
 import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -129,16 +128,14 @@ function ProductOptionGroupDetailPage() {
                 {creatingNewEntity ? <Trans>New product option group</Trans> : (entity?.name ?? '')}
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateProduct', 'UpdateCatalog']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateProduct', 'UpdateCatalog']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx

@@ -1,7 +1,6 @@
 import { SlugInput } from '@/vdb/components/data-input/index.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
@@ -9,11 +8,11 @@ import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import { extendDetailFormQuery } from '@/vdb/framework/document-extension/extend-detail-form-query.js';
 import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -150,16 +149,14 @@ function ProductOptionDetailPage() {
                 {creatingNewEntity ? <Trans>New product option</Trans> : ((entity as any)?.name ?? '')}
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateProduct', 'UpdateCatalog']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateProduct', 'UpdateCatalog']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="side" blockId="option-group-info">

+ 3 - 3
packages/dashboard/src/app/routes/_authenticated/_profile/profile.tsx

@@ -3,11 +3,11 @@ import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js'
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -74,14 +74,14 @@ function ProfilePage() {
                 <Trans>Profile</Trans>
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
+                <ActionBarItem itemId="save-button">
                     <Button
                         type="submit"
                         disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
                     >
                         <Trans>Update</Trans>
                     </Button>
-                </PageActionBarRight>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_promotions/promotions.tsx

@@ -1,9 +1,8 @@
 import { BooleanDisplayBadge } from '@/vdb/components/data-display/boolean.js';
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { RichTextDescriptionCell } from '@/vdb/components/shared/table-cell/order-table-cell-components.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -79,16 +78,14 @@ function PromotionListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreatePromotion']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon className="mr-2 h-4 w-4" />
-                            <Trans>New Promotion</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreatePromotion']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon className="mr-2 h-4 w-4" />
+                        <Trans>New Promotion</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx

@@ -3,18 +3,17 @@ import { NumberInput } from '@/vdb/components/data-input/number-input.js';
 import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -127,16 +126,14 @@ function PromotionDetailPage() {
         <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
             <PageTitle>{creatingNewEntity ? <Trans>New promotion</Trans> : (entity?.name ?? '')}</PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdatePromotion']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdatePromotion']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="side" blockId="enabled">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_roles/roles.tsx

@@ -1,11 +1,10 @@
 import { ChannelCodeLabel } from '@/vdb/components/shared/channel-code-label.js';
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { RoleCodeLabel } from '@/vdb/components/shared/role-code-label.js';
 import { Badge } from '@/vdb/components/ui/badge.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { CUSTOMER_ROLE_CODE, SUPER_ADMIN_ROLE_CODE } from '@/vdb/constants.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -84,16 +83,14 @@ function RoleListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreateAdministrator']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon className="mr-2 h-4 w-4" />
-                            <Trans>New Role</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateAdministrator']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon className="mr-2 h-4 w-4" />
+                        <Trans>New Role</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_roles/roles_.$id.tsx

@@ -1,15 +1,14 @@
 import { ChannelSelector } from '@/vdb/components/shared/channel-selector.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -78,16 +77,14 @@ function RoleDetailPage() {
         <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
             <PageTitle>{creatingNewEntity ? <Trans>New role</Trans> : (entity?.description ?? '')}</PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateAdministrator']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateAdministrator']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_sellers/sellers.tsx

@@ -1,7 +1,6 @@
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -41,16 +40,14 @@ function SellerListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreateSeller']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon className="mr-2 h-4 w-4" />
-                            <Trans>New Seller</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateSeller']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon className="mr-2 h-4 w-4" />
+                        <Trans>New Seller</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx

@@ -1,14 +1,13 @@
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/vdb/components/ui/form.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -72,16 +71,14 @@ function SellerDetailPage() {
         <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
             <PageTitle>{creatingNewEntity ? <Trans>New seller</Trans> : (entity?.name ?? '')}</PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateSeller']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateSeller']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 11 - 12
packages/dashboard/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx

@@ -1,8 +1,7 @@
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { RichTextDescriptionCell } from '@/vdb/components/shared/table-cell/order-table-cell-components.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -60,17 +59,17 @@ function ShippingMethodListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
+            <ActionBarItem itemId="test-shipping-button">
                 <TestShippingMethodsSheet />
-                <PermissionGuard requires={['CreateShippingMethod']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon className="mr-2 h-4 w-4" />
-                            <Trans>New Shipping Method</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            </ActionBarItem>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateShippingMethod']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon className="mr-2 h-4 w-4" />
+                        <Trans>New Shipping Method</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 19 - 20
packages/dashboard/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx

@@ -1,17 +1,16 @@
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { TranslatableFormFieldWrapper } from '@/vdb/components/shared/translatable-form-field.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { Textarea } from '@/vdb/components/ui/textarea.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -114,25 +113,25 @@ function ShippingMethodDetailPage() {
                 {creatingNewEntity ? <Trans>New shipping method</Trans> : (entity?.name ?? '')}
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    {!creatingNewEntity && entity && (
+                {!creatingNewEntity && entity && (
+                    <ActionBarItem itemId="test-shipping-button">
                         <TestSingleShippingMethodSheet checker={checker} calculator={calculator} />
-                    )}
-                    <PermissionGuard requires={['UpdateShippingMethod']}>
-                        <Button
-                            type="submit"
-                            disabled={
-                                !form.formState.isDirty ||
-                                !form.formState.isValid ||
-                                isPending ||
-                                !checker?.code ||
-                                !calculator?.code
-                            }
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                    </ActionBarItem>
+                )}
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateShippingMethod']}>
+                    <Button
+                        type="submit"
+                        disabled={
+                            !form.formState.isDirty ||
+                            !form.formState.isValid ||
+                            isPending ||
+                            !checker?.code ||
+                            !calculator?.code
+                        }
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx

@@ -1,7 +1,6 @@
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -50,16 +49,14 @@ function StockLocationListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreateStockLocation']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon className="mr-2 h-4 w-4" />
-                            <Trans>New Stock Location</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateStockLocation']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon className="mr-2 h-4 w-4" />
+                        <Trans>New Stock Location</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx

@@ -1,16 +1,15 @@
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { Textarea } from '@/vdb/components/ui/textarea.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -90,16 +89,14 @@ function StockLocationDetailPage() {
                 {creatingNewEntity ? <Trans>New stock location</Trans> : (entity?.name ?? '')}
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateStockLocation']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateStockLocation']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 3 - 3
packages/dashboard/src/app/routes/_authenticated/_system/job-queue.tsx

@@ -6,7 +6,7 @@ import {
     DropdownMenuItem,
     DropdownMenuTrigger,
 } from '@/vdb/components/ui/dropdown-menu.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { api } from '@/vdb/graphql/api.js';
 import { Trans, useLingui } from '@lingui/react/macro';
@@ -227,7 +227,7 @@ function JobQueuePage() {
                 refreshRef.current = refresher;
             }}
         >
-            <PageActionBarRight>
+            <ActionBarItem itemId="auto-refresh-button">
                 <DropdownMenu>
                     <DropdownMenuTrigger asChild>
                         <Button variant="outline" size="sm" className="gap-2">
@@ -250,7 +250,7 @@ function JobQueuePage() {
                         ))}
                     </DropdownMenuContent>
                 </DropdownMenu>
-            </PageActionBarRight>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx

@@ -1,8 +1,7 @@
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Badge } from '@/vdb/components/ui/badge.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -54,16 +53,14 @@ function TaxCategoryListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreateTaxCategory']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon />
-                            <Trans>New Tax Category</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateTaxCategory']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon />
+                        <Trans>New Tax Category</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx

@@ -1,16 +1,15 @@
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -90,16 +89,14 @@ function TaxCategoryDetailPage() {
                 {creatingNewEntity ? <Trans>New tax category</Trans> : (entity?.name ?? '')}
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateTaxCategory']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateTaxCategory']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx

@@ -1,8 +1,7 @@
 import { BooleanDisplayBadge } from '@/vdb/components/data-display/boolean.js';
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { api } from '@/vdb/graphql/api.js';
 import { Trans, useLingui } from '@lingui/react/macro';
@@ -104,16 +103,14 @@ function TaxRateListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreateTaxRate']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon />
-                            <Trans>New Tax Rate</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateTaxRate']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon />
+                        <Trans>New Tax Rate</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx

@@ -1,7 +1,6 @@
 import { AffixedInput } from '@/vdb/components/data-input/affixed-input.js';
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { TaxCategorySelector } from '@/vdb/components/shared/tax-category-selector.js';
 import { ZoneSelector } from '@/vdb/components/shared/zone-selector.js';
 import { Button } from '@/vdb/components/ui/button.js';
@@ -9,11 +8,11 @@ import { Input } from '@/vdb/components/ui/input.js';
 import { Switch } from '@/vdb/components/ui/switch.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -86,16 +85,14 @@ function TaxRateDetailPage() {
         <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
             <PageTitle>{creatingNewEntity ? <Trans>New tax rate</Trans> : (entity?.name ?? '')}</PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateTaxRate']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateTaxRate']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="side" blockId="enabled">

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_zones/zones.tsx

@@ -1,7 +1,6 @@
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
-import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
+import { ActionBarItem } from '@/vdb/framework/layout-engine/page-layout.js';
 import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans, useLingui } from '@lingui/react/macro';
 import { createFileRoute, Link } from '@tanstack/react-router';
@@ -48,16 +47,14 @@ function ZoneListPage() {
                 },
             ]}
         >
-            <PageActionBarRight>
-                <PermissionGuard requires={['CreateZone']}>
-                    <Button asChild>
-                        <Link to="./new">
-                            <PlusIcon />
-                            <Trans>New Zone</Trans>
-                        </Link>
-                    </Button>
-                </PermissionGuard>
-            </PageActionBarRight>
+            <ActionBarItem itemId="create-button" requiresPermission={['CreateZone']}>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon />
+                        <Trans>New Zone</Trans>
+                    </Link>
+                </Button>
+            </ActionBarItem>
         </ListPage>
     );
 }

+ 9 - 12
packages/dashboard/src/app/routes/_authenticated/_zones/zones_.$id.tsx

@@ -1,15 +1,14 @@
 import { ErrorPage } from '@/vdb/components/shared/error-page.js';
 import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
-import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { Input } from '@/vdb/components/ui/input.js';
 import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
 import {
+    ActionBarItem,
     CustomFieldsPageBlock,
     DetailFormGrid,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageBlock,
     PageLayout,
     PageTitle,
@@ -76,16 +75,14 @@ function ZoneDetailPage() {
         <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
             <PageTitle>{creatingNewEntity ? <Trans>New zone</Trans> : (entity?.name ?? '')}</PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
-                    <PermissionGuard requires={['UpdateZone']}>
-                        <Button
-                            type="submit"
-                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
-                        >
-                            {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
-                        </Button>
-                    </PermissionGuard>
-                </PageActionBarRight>
+                <ActionBarItem itemId="save-button" requiresPermission={['UpdateZone']}>
+                    <Button
+                        type="submit"
+                        disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                    >
+                        {creatingNewEntity ? <Trans>Create</Trans> : <Trans>Update</Trans>}
+                    </Button>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <PageBlock column="main" blockId="main-form">

+ 5 - 3
packages/dashboard/src/app/routes/_authenticated/index.tsx

@@ -12,10 +12,10 @@ import {
 } from '@/vdb/framework/dashboard-widget/widget-filters-context.js';
 import { DashboardWidgetInstance } from '@/vdb/framework/extension-api/types/widgets.js';
 import {
+    ActionBarItem,
     FullWidthPageBlock,
     Page,
     PageActionBar,
-    PageActionBarRight,
     PageLayout,
     PageTitle,
 } from '@/vdb/framework/layout-engine/page-layout.js';
@@ -181,19 +181,21 @@ function DashboardPage() {
                 <Trans>Insights</Trans>
             </PageTitle>
             <PageActionBar>
-                <PageActionBarRight>
+                <ActionBarItem itemId="date-range-picker">
                     <DateRangePicker
                         dateRange={dateRange}
                         onDateRangeChange={setDateRange}
                         className="mr-2"
                     />
+                </ActionBarItem>
+                <ActionBarItem itemId="edit-layout-button">
                     <Button
                         variant={editMode ? 'default' : 'outline'}
                         onClick={() => setEditMode(prev => !prev)}
                     >
                         {editMode ? t`Save Layout` : t`Edit Layout`}
                     </Button>
-                </PageActionBarRight>
+                </ActionBarItem>
             </PageActionBar>
             <PageLayout>
                 <FullWidthPageBlock blockId="widgets">

+ 1 - 1
packages/dashboard/src/lib/components/data-table/column-header-wrapper.tsx

@@ -72,7 +72,7 @@ export function ColumnHeaderWrapper({ children, columnId }: Readonly<ColumnHeade
                 >
                     <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
                         <PopoverTrigger asChild>
-                            <DevModeButton className={`h-5 w-5`} />
+                            <DevModeButton className={`h-5 w-5 end-1 top-1`} />
                         </PopoverTrigger>
                         <PopoverContent className="w-48 p-3">
                             <div className="space-y-2">

+ 7 - 3
packages/dashboard/src/lib/components/layout/dev-mode-indicator.tsx

@@ -2,14 +2,18 @@ import { Badge } from '@/vdb/components/ui/badge.js';
 import { Button } from '@/vdb/components/ui/button.js';
 import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
 import { Trans } from '@lingui/react/macro';
-import { CodeXmlIcon, XIcon } from 'lucide-react';
+import { SquareDashedMousePointer, XIcon } from 'lucide-react';
 
 export function DevModeIndicator() {
     const { setDevMode } = useUserSettings();
     return (
         <Badge className="bg-dev-mode text-background">
-            <CodeXmlIcon className="w-6 h-6" />
-            <Trans>Dev Mode</Trans>
+            <div>
+                <SquareDashedMousePointer className="w-4 h-4" />
+            </div>
+            <div>
+                <Trans>Dev Mode</Trans>
+            </div>
             <Button variant="ghost" size="icon-xs" onClick={() => setDevMode(false)}>
                 <XIcon className="w-4 h-4" />
             </Button>

+ 1 - 1
packages/dashboard/src/lib/components/layout/nav-item-wrapper.tsx

@@ -79,7 +79,7 @@ export function NavItemWrapper({
                 >
                     <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
                         <PopoverTrigger asChild>
-                            <DevModeButton className={`h-6 w-6`} />
+                            <DevModeButton className={`h-5 w-5 top-0 end-0`} />
                         </PopoverTrigger>
                         <PopoverContent className="w-48 p-3">
                             <div className="space-y-2">

+ 8 - 3
packages/dashboard/src/lib/components/shared/asset/asset-gallery.tsx

@@ -13,6 +13,7 @@ import {
     PaginationPrevious,
 } from '@/vdb/components/ui/pagination.js';
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
+import { ActionBarItem, PageActionBar } from '@/vdb/framework/layout-engine/page-layout.js';
 import { api } from '@/vdb/graphql/api.js';
 import { assetFragment, AssetFragment } from '@/vdb/graphql/fragments.js';
 import { graphql } from '@/vdb/graphql/graphql.js';
@@ -357,9 +358,13 @@ export function AssetGallery({
                                 <SelectItem value={AssetType.BINARY}>Binary</SelectItem>
                             </SelectContent>
                         </Select>
-                        <Button onClick={openFileDialog} className="whitespace-nowrap">
-                            <Upload className="h-4 w-4 mr-2" /> <Trans>Upload</Trans>
-                        </Button>
+                        <PageActionBar>
+                            <ActionBarItem itemId="upload-assets-button">
+                                <Button onClick={openFileDialog} className="whitespace-nowrap">
+                                    <Upload className="h-4 w-4 mr-2" /> <Trans>Upload</Trans>
+                                </Button>
+                            </ActionBarItem>
+                        </PageActionBar>
                     </div>
 
                     {hasTags && (

+ 33 - 0
packages/dashboard/src/lib/framework/extension-api/types/layout.ts

@@ -52,8 +52,41 @@ export interface DashboardActionBarItem {
      * Any permissions that are required to display this action bar item.
      */
     requiresPermission?: string | string[];
+    /**
+     * @description
+     * A unique identifier for this action bar item. This is required if you want
+     * other extensions to be able to position their items relative to this one.
+     *
+     * @since 3.5.2
+     */
+    id?: string;
+    /**
+     * @description
+     * Position this item relative to another action bar item. The `itemId` should
+     * match the `id` of an existing action bar item (either a built-in item or one
+     * added by another extension).
+     *
+     * - `'before'`: Place this item before the target item
+     * - `'after'`: Place this item after the target item
+     * - `'replace'`: Replace the target item entirely with this item
+     *
+     * @since 3.5.2
+     */
+    position?: ActionBarItemPosition;
 }
 
+/**
+ * @description
+ * The relative position of an ActionBar item. This is determined by finding an existing
+ * action bar item by its `id`, and then specifying whether your custom item should come
+ * before, after, or completely replace that item.
+ *
+ * @docsCategory extensions-api
+ * @docsPage ActionBar
+ * @since 3.5.2
+ */
+export type ActionBarItemPosition = { itemId: string; order: 'before' | 'after' | 'replace' };
+
 /**
  * @description
  * The relative position of a PageBlock. This is determined by finding an existing

+ 185 - 0
packages/dashboard/src/lib/framework/layout-engine/action-bar-item-wrapper.tsx

@@ -0,0 +1,185 @@
+import { CopyableText } from '@/vdb/components/shared/copyable-text.js';
+import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
+import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
+import { usePage } from '@/vdb/hooks/use-page.js';
+import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
+import { cn } from '@/vdb/lib/utils.js';
+import React, { useEffect, useState } from 'react';
+import { DevModeButton } from './dev-mode-button.js';
+
+// Singleton state for hover tracking across all action bar items
+let globalHoveredActionBarItemId: string | null = null;
+const actionBarHoverListeners: Set<(id: string | null) => void> = new Set();
+
+const setGlobalHoveredActionBarItemId = (id: string | null) => {
+    globalHoveredActionBarItemId = id;
+    actionBarHoverListeners.forEach(listener => listener(id));
+};
+
+/**
+ * Internal component that renders the dev-mode wrapper with hover highlight and popover.
+ * Shared between ActionBarItem and ActionBarItemWrapper to eliminate duplication.
+ */
+function DevModeActionBarWrapper({
+    children,
+    itemId,
+}: Readonly<{
+    children: React.ReactNode;
+    itemId: string;
+}>) {
+    const page = usePage();
+    const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+    const [hoveredId, setHoveredId] = useState<string | null>(globalHoveredActionBarItemId);
+
+    // Generate a unique tracking ID that includes the page context
+    const trackingId = `${page.pageId ?? 'unknown'}-actionbar-${itemId}`;
+    const isHovered = hoveredId === trackingId;
+
+    // Subscribe to global hover changes
+    useEffect(() => {
+        const listener = (newHoveredId: string | null) => {
+            setHoveredId(newHoveredId);
+        };
+        actionBarHoverListeners.add(listener);
+        return () => {
+            actionBarHoverListeners.delete(listener);
+        };
+    }, []);
+
+    const handleMouseEnter = () => {
+        setGlobalHoveredActionBarItemId(trackingId);
+    };
+
+    const handleMouseLeave = () => {
+        setGlobalHoveredActionBarItemId(null);
+    };
+
+    return (
+        <div
+            className={cn(
+                'ring-1 ring-transparent rounded transition-all delay-50 relative',
+                isHovered || isPopoverOpen ? 'ring-dev-mode ring-offset-1 ring-offset-background' : '',
+            )}
+            onMouseEnter={handleMouseEnter}
+            onMouseLeave={handleMouseLeave}
+        >
+            <div
+                className={cn(
+                    'absolute -top-1 -right-1 transition-all delay-50 z-10',
+                    isHovered || isPopoverOpen ? 'visible' : 'invisible',
+                )}
+            >
+                <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
+                    <PopoverTrigger asChild>
+                        <DevModeButton className="h-5 w-5 top-0 -start-4" />
+                    </PopoverTrigger>
+                    <PopoverContent className="w-40 p-2">
+                        <div className="space-y-1.5">
+                            {page.pageId && (
+                                <div className="text-xs">
+                                    <div className="text-muted-foreground mb-0.5">pageId</div>
+                                    <CopyableText text={page.pageId} />
+                                </div>
+                            )}
+                            <div className="text-xs">
+                                <div className="text-muted-foreground mb-0.5">itemId</div>
+                                <CopyableText text={itemId} />
+                            </div>
+                        </div>
+                    </PopoverContent>
+                </Popover>
+            </div>
+            {children}
+        </div>
+    );
+}
+
+/**
+ * @description
+ * Props for the ActionBarItem component.
+ *
+ * @docsCategory page-layout
+ * @docsPage PageActionBar
+ * @since 3.5.2
+ */
+export interface ActionBarItemProps {
+    /**
+     * @description
+     * The content of the action bar item, typically a Button component.
+     */
+    children: React.ReactNode;
+    /**
+     * @description
+     * A unique identifier for this action bar item. This ID is used by extensions
+     * to position their items relative to this one via `position.itemId`.
+     *
+     * Note: Extensions should use this exact `itemId` value in their `position.itemId`
+     * field to target this item.
+     */
+    itemId: string;
+    /**
+     * @description
+     * If provided, the logged-in user must have one or more of the specified
+     * permissions in order for the item to render.
+     */
+    requiresPermission?: string | string[];
+}
+
+/**
+ * @description
+ * A component for wrapping action bar items with a unique ID. This should be used inside
+ * the {@link PageActionBarRight} component. Each item is given an `itemId` which allows
+ * extensions to position their items relative to it using `position.itemId`.
+ *
+ * In developer mode, hovering over the item will show a popover with the `pageId` and `itemId`,
+ * making it easy to discover the correct IDs for extension positioning.
+ *
+ * @example
+ * ```tsx
+ * <PageActionBarRight>
+ *     <ActionBarItem itemId="save-button" requiresPermission={['UpdateProduct']}>
+ *         <Button type="submit">Save</Button>
+ *     </ActionBarItem>
+ * </PageActionBarRight>
+ * ```
+ *
+ * @docsCategory page-layout
+ * @docsPage PageActionBar
+ * @since 3.5.2
+ */
+export function ActionBarItem({ children, itemId, requiresPermission }: Readonly<ActionBarItemProps>) {
+    const { settings } = useUserSettings();
+
+    const content = requiresPermission ? (
+        <PermissionGuard requires={requiresPermission}>{children}</PermissionGuard>
+    ) : (
+        children
+    );
+
+    if (settings.devMode) {
+        return <DevModeActionBarWrapper itemId={itemId}>{content}</DevModeActionBarWrapper>;
+    }
+    return <>{content}</>;
+}
+
+/**
+ * Internal wrapper component used by PageActionBarRight to wrap extension items
+ * with dev-mode location information. Unlike ActionBarItem, this does not handle
+ * permissions (those are handled by PageActionBarItem).
+ *
+ * @internal
+ */
+export function ActionBarItemWrapper({
+    children,
+    itemId,
+}: Readonly<{
+    children: React.ReactNode;
+    itemId: string;
+}>) {
+    const { settings } = useUserSettings();
+
+    if (settings.devMode) {
+        return <DevModeActionBarWrapper itemId={itemId}>{children}</DevModeActionBarWrapper>;
+    }
+    return <>{children}</>;
+}

+ 15 - 13
packages/dashboard/src/lib/framework/layout-engine/dev-mode-button.tsx

@@ -1,24 +1,26 @@
 import { Button } from '@/vdb/components/ui/button.js';
 import { cn } from '@/vdb/lib/utils.js';
-import { CodeXmlIcon } from 'lucide-react';
+import { Locate } from 'lucide-react';
 import { forwardRef } from 'react';
 
 export const DevModeButton = forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
     (props, ref) => {
         const { className, ...rest } = props;
         return (
-            <Button
-                ref={ref}
-                variant="secondary"
-                size="icon"
-                className={cn(
-                    'h-8 w-8 rounded-full bg-dev-mode/20 hover:bg-dev-mode/30 border border-dev-mode/20 shadow-sm',
-                    className,
-                )}
-                {...rest}
-            >
-                <CodeXmlIcon className="text-dev-mode w-4 h-4" />
-            </Button>
+            <div className="relative">
+                <Button
+                    ref={ref}
+                    variant="secondary"
+                    size="icon"
+                    className={cn(
+                        'h-6 w-6 absolute z-50 rounded-md bg-background text-dev-mode/70 hover:bg-background hover:text-dev-mode border border-dev-mode shadow-sm',
+                        className,
+                    )}
+                    {...rest}
+                >
+                    <Locate className="w-4 h-4" />
+                </Button>
+            </div>
         );
     },
 );

+ 3 - 1
packages/dashboard/src/lib/framework/layout-engine/location-wrapper.tsx

@@ -86,7 +86,9 @@ export function LocationWrapper({ children, identifier }: Readonly<LocationWrapp
                 >
                     <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
                         <PopoverTrigger asChild>
-                            <DevModeButton />
+                            <DevModeButton
+                                className={isPageWrapper ? '-top-8 -end-1 border-2' : '-top-4 -end-4'}
+                            />
                         </PopoverTrigger>
                         <PopoverContent className="w-48 p-3">
                             <div className="space-y-2">

+ 237 - 41
packages/dashboard/src/lib/framework/layout-engine/page-layout.tsx

@@ -11,7 +11,8 @@ import { CheckIcon, CopyIcon, EllipsisVerticalIcon, InfoIcon } from 'lucide-reac
 import React, { ComponentProps, useMemo, useState } from 'react';
 import { Control, UseFormReturn } from 'react-hook-form';
 
-import { DashboardActionBarItem } from '../extension-api/types/layout.js';
+import { ActionBarItemPosition, DashboardActionBarItem } from '../extension-api/types/layout.js';
+import { ActionBarItem, ActionBarItemProps, ActionBarItemWrapper } from './action-bar-item-wrapper.js';
 
 import { Button } from '@/vdb/components/ui/button.js';
 import {
@@ -91,7 +92,7 @@ export interface PageProps extends ComponentProps<'div'> {
 export function Page({ children, pageId, entity, form, submitHandler, ...props }: Readonly<PageProps>) {
     const childArray = React.Children.toArray(children);
 
-    const pageTitle = childArray.find(child => React.isValidElement(child) && child.type === PageTitle);
+    const pageTitle = childArray.find(child => isOfType(child, PageTitle));
     const pageActionBar = childArray.find(child => isOfType(child, PageActionBar));
 
     const pageContent = childArray.filter(
@@ -100,7 +101,7 @@ export function Page({ children, pageId, entity, form, submitHandler, ...props }
 
     const pageHeader = (
         <div className="flex items-center justify-between">
-            {pageTitle}
+            {pageTitle ?? <div />}
             {pageActionBar}
         </div>
     );
@@ -337,44 +338,226 @@ export function PageTitle({ children }: Readonly<{ children: React.ReactNode }>)
     return <h1 className="text-2xl font-semibold">{children}</h1>;
 }
 
+type InlineDropdownItem = Omit<DashboardActionBarItem, 'type' | 'pageId'>;
+
 /**
- * @description *
+ * @description
  * A component for displaying the main actions for a page. This should be used inside the {@link Page} component.
- * It should be used in conjunction with the {@link PageActionBarLeft} and {@link PageActionBarRight} components
- * as direct children.
+ *
+ * You can add action bar items by including {@link ActionBarItem} components as direct children.
+ * For backwards compatibility, {@link PageActionBarLeft} and {@link PageActionBarRight} are also supported.
+ *
+ * @example
+ * ```tsx
+ * <PageActionBar>
+ *     <ActionBarItem itemId="save-button" requiresPermission={['UpdateProduct']}>
+ *         <Button type="submit">Update</Button>
+ *     </ActionBarItem>
+ * </PageActionBar>
+ * ```
  *
  * @docsCategory page-layout
  * @docsPage PageActionBar
  * @docsWeight 0
  * @since 3.3.0
  */
-export function PageActionBar({ children }: Readonly<{ children: React.ReactNode }>) {
-    let childArray = React.Children.toArray(children);
+export function PageActionBar({
+    children,
+    dropdownMenuItems,
+}: Readonly<{
+    children: React.ReactNode;
+    /**
+     * @description
+     * Optional dropdown menu items to display in the action bar's context menu.
+     */
+    dropdownMenuItems?: InlineDropdownItem[];
+}>) {
+    const page = usePage();
+    const actionBarItems = page.pageId ? getDashboardActionBarItems(page.pageId) : [];
+    const childArray = React.Children.toArray(children);
 
+    // Extract different child types
     const leftContent = childArray.filter(child => isOfType(child, PageActionBarLeft));
     const rightContent = childArray.filter(child => isOfType(child, PageActionBarRight));
 
+    // Collect ActionBarItem children (direct or from PageActionBarRight)
+    const actionBarItemChildren: React.ReactElement<ActionBarItemProps>[] = [];
+    // Collect plain children (not ActionBarItem, not PageActionBarLeft/Right)
+    const plainChildren: React.ReactNode[] = [];
+    // Collect dropdownMenuItems from PageActionBarRight (backwards compat)
+    let legacyDropdownMenuItems: InlineDropdownItem[] = [];
+
+    // Direct children (new pattern)
+    childArray.forEach(child => {
+        if (isActionBarItem(child)) {
+            actionBarItemChildren.push(child);
+        } else if (!isOfType(child, PageActionBarLeft) && !isOfType(child, PageActionBarRight)) {
+            // Plain children (buttons etc.) that aren't ActionBarItem or layout components
+            plainChildren.push(child);
+        }
+    });
+
+    // Children and dropdownMenuItems from PageActionBarRight (backwards compat)
+    rightContent.forEach(rightChild => {
+        if (React.isValidElement(rightChild)) {
+            const props = rightChild.props as {
+                children?: React.ReactNode;
+                dropdownMenuItems?: InlineDropdownItem[];
+            };
+            React.Children.forEach(props.children, child => {
+                if (isActionBarItem(child)) {
+                    actionBarItemChildren.push(child);
+                } else {
+                    // Plain children (raw buttons etc.)
+                    plainChildren.push(child);
+                }
+            });
+            // Extract dropdownMenuItems from PageActionBarRight props
+            if (props.dropdownMenuItems) {
+                legacyDropdownMenuItems = [...legacyDropdownMenuItems, ...props.dropdownMenuItems];
+            }
+        }
+    });
+
+    // Separate button items from dropdown items
+    const extensionButtonItems = actionBarItems.filter(item => item.type !== 'dropdown');
+    const allDropdownMenuItems = [...(dropdownMenuItems ?? []), ...legacyDropdownMenuItems];
+    const actionBarDropdownItems = [
+        ...allDropdownMenuItems.map(item => ({
+            ...item,
+            pageId: page.pageId ?? '',
+            type: 'dropdown' as const,
+        })),
+        ...actionBarItems.filter(item => item.type === 'dropdown'),
+    ];
+
+    // Merge and sort inline items with extension items
+    const mergedItems = mergeAndSortActionBarItems(actionBarItemChildren, extensionButtonItems);
+
+    // Determine if we should render the right section
+    const hasRightContent =
+        mergedItems.length > 0 ||
+        plainChildren.length > 0 ||
+        actionBarDropdownItems.length > 0 ||
+        page.entity;
+
     return (
         <div className={cn('flex gap-2', leftContent.length > 0 ? 'justify-between' : 'justify-end')}>
             {leftContent.length > 0 && <div className="flex justify-start gap-2">{leftContent}</div>}
-            {rightContent.length > 0 && <div className="flex justify-end gap-2">{rightContent}</div>}
+            {hasRightContent && (
+                <div className="flex justify-end gap-2">
+                    {/* Plain children (buttons etc. not wrapped in ActionBarItem) */}
+                    {plainChildren.map((child, index) => (
+                        <React.Fragment key={`plain-${index}`}>{child}</React.Fragment>
+                    ))}
+                    {/* Merged ActionBarItem children with extensions */}
+                    {mergedItems.map((mergedItem, index) => {
+                        if (mergedItem.type === 'inline') {
+                            return React.cloneElement(mergedItem.element, {
+                                key: `inline-${mergedItem.element.props.itemId}`,
+                            });
+                        } else {
+                            const extItem = mergedItem.item;
+                            const itemId = extItem.id ?? `extension-${extItem.component.name || index}`;
+                            return (
+                                <ActionBarItemWrapper
+                                    key={`ext-${extItem.id ?? extItem.pageId}-${index}`}
+                                    itemId={itemId}
+                                >
+                                    <PageActionBarItem item={extItem} page={page} />
+                                </ActionBarItemWrapper>
+                            );
+                        }
+                    })}
+                    {actionBarDropdownItems.length > 0 && (
+                        <PageActionBarDropdown items={actionBarDropdownItems} page={page} />
+                    )}
+                    <EntityInfoDropdown entity={page.entity} />
+                </div>
+            )}
         </div>
     );
 }
 
 /**
  * @description
- * The PageActionBarLeft component should be used to display the left content of the action bar.
+ * The PageActionBarLeft component is not used and will be removed in a future version.
  *
  * @docsCategory page-layout
  * @docsPage PageActionBar
+ * @deprecated
  * @since 3.3.0
  */
 export function PageActionBarLeft({ children }: Readonly<{ children: React.ReactNode }>) {
     return <div className="flex justify-start gap-2">{children}</div>;
 }
 
-type InlineDropdownItem = Omit<DashboardActionBarItem, 'type' | 'pageId'>;
+/**
+ * Checks if a React child is an ActionBarItem component.
+ */
+function isActionBarItem(child: unknown): child is React.ReactElement<ActionBarItemProps> {
+    return React.isValidElement(child) && isOfType(child, ActionBarItem);
+}
+
+/**
+ * Represents a merged action bar item that can be either inline (ActionBarItem child) or from an extension.
+ * Used internally for sorting and rendering.
+ */
+type MergedActionBarItem =
+    | { type: 'inline'; element: React.ReactElement<ActionBarItemProps> }
+    | { type: 'extension'; item: DashboardActionBarItem };
+
+/**
+ * Merges inline ActionBarItem children with extension items, applying position-based ordering.
+ * Uses the same priority sorting as page blocks: before=1, replace=2, after=3.
+ */
+function mergeAndSortActionBarItems(
+    inlineElements: React.ReactElement<ActionBarItemProps>[],
+    extensionItems: DashboardActionBarItem[],
+): MergedActionBarItem[] {
+    const result: MergedActionBarItem[] = [];
+
+    // First, add extension items WITHOUT a position (they go first, preserving current behavior)
+    const unpositionedExtensions = extensionItems.filter(ext => !ext.position);
+    for (const ext of unpositionedExtensions) {
+        result.push({ type: 'extension', item: ext });
+    }
+
+    // Process each inline element and find extension items targeting it
+    for (const inlineElement of inlineElements) {
+        const itemId = inlineElement.props.itemId;
+        const matchingExtensions = extensionItems.filter(ext => ext.position?.itemId === itemId);
+
+        // Sort by order priority: before=1, replace=2, after=3
+        const sortedExtensions = matchingExtensions.sort((a, b) => {
+            const orderPriority: Record<ActionBarItemPosition['order'], number> = {
+                before: 1,
+                replace: 2,
+                after: 3,
+            };
+            return orderPriority[a.position!.order] - orderPriority[b.position!.order];
+        });
+
+        const hasReplacement = sortedExtensions.some(ext => ext.position?.order === 'replace');
+
+        let inlineInserted = false;
+        for (const ext of sortedExtensions) {
+            // Insert inline element before the first non-"before" extension (if not replaced)
+            if (!inlineInserted && !hasReplacement && ext.position?.order !== 'before') {
+                result.push({ type: 'inline', element: inlineElement });
+                inlineInserted = true;
+            }
+            result.push({ type: 'extension', item: ext });
+        }
+
+        // If all extensions were "before" or there were no extensions, add inline at the end
+        if (!inlineInserted && !hasReplacement) {
+            result.push({ type: 'inline', element: inlineElement });
+        }
+    }
+
+    return result;
+}
 
 function EntityInfoDropdown({ entity }: Readonly<{ entity: any }>) {
     const [copiedField, setCopiedField] = useState<string | null>(null);
@@ -457,7 +640,28 @@ function EntityInfoDropdown({ entity }: Readonly<{ entity: any }>) {
 
 /**
  * @description
- * The PageActionBarRight component should be used to display the right content of the action bar.
+ * The PageActionBarRight component is used to display the right content of the action bar.
+ *
+ * @deprecated Use {@link ActionBarItem} children directly in {@link PageActionBar} instead.
+ *
+ * @example
+ * ```tsx
+ * // Old pattern (deprecated)
+ * <PageActionBar>
+ *     <PageActionBarRight>
+ *         <ActionBarItem itemId="save-button">
+ *             <Button type="submit">Update</Button>
+ *         </ActionBarItem>
+ *     </PageActionBarRight>
+ * </PageActionBar>
+ *
+ * // New pattern (recommended)
+ * <PageActionBar>
+ *     <ActionBarItem itemId="save-button" requiresPermission={['UpdateProduct']}>
+ *         <Button type="submit">Update</Button>
+ *     </ActionBarItem>
+ * </PageActionBar>
+ * ```
  *
  * @docsCategory page-layout
  * @docsPage PageActionBar
@@ -465,35 +669,25 @@ function EntityInfoDropdown({ entity }: Readonly<{ entity: any }>) {
  */
 export function PageActionBarRight({
     children,
-    dropdownMenuItems,
+    dropdownMenuItems: _dropdownMenuItems,
 }: Readonly<{
-    children: React.ReactNode;
+    /**
+     * @description
+     * ActionBarItem components that will be rendered in the action bar.
+     * Each item should have a unique `itemId` for extension targeting.
+     */
+    children?: React.ReactNode;
+    /**
+     * @description
+     * Optional dropdown menu items. These are now extracted and rendered by PageActionBar.
+     * @deprecated Pass `dropdownMenuItems` directly to {@link PageActionBar} instead.
+     */
     dropdownMenuItems?: InlineDropdownItem[];
 }>) {
-    const page = usePage();
-    const actionBarItems = page.pageId ? getDashboardActionBarItems(page.pageId) : [];
-    const actionBarButtonItems = actionBarItems.filter(item => item.type !== 'dropdown');
-    const actionBarDropdownItems = [
-        ...(dropdownMenuItems ?? []).map(item => ({
-            ...item,
-            pageId: page.pageId ?? '',
-            type: 'dropdown' as const,
-        })),
-        ...actionBarItems.filter(item => item.type === 'dropdown'),
-    ];
-
-    return (
-        <div className="flex justify-end gap-2">
-            {actionBarButtonItems.map((item, index) => (
-                <PageActionBarItem key={item.pageId + index} item={item} page={page} />
-            ))}
-            {children}
-            {actionBarDropdownItems.length > 0 && (
-                <PageActionBarDropdown items={actionBarDropdownItems} page={page} />
-            )}
-            <EntityInfoDropdown entity={page.entity} />
-        </div>
-    );
+    // This is now a passthrough wrapper for backwards compatibility.
+    // The actual logic is handled by PageActionBar which extracts ActionBarItem
+    // children and dropdownMenuItems from PageActionBarRight.
+    return <>{children}</>;
 }
 
 function PageActionBarItem({
@@ -615,9 +809,7 @@ export function PageBlock({
                             {description && <CardDescription>{description}</CardDescription>}
                         </CardHeader>
                     ) : null}
-                    <CardContent className={cn(!title ? 'pt-6' : '', 'overflow-auto')}>
-                        {children}
-                    </CardContent>
+                    <CardContent className={cn(!title ? 'pt-6' : '', '')}>{children}</CardContent>
                 </Card>
             </LocationWrapper>
         </PageBlockContext.Provider>
@@ -698,3 +890,7 @@ export function isOfType(el: unknown, type: React.FunctionComponent<any>): boole
     }
     return false;
 }
+
+// Re-export ActionBarItem for convenience alongside other page layout components
+export { ActionBarItem } from './action-bar-item-wrapper.js';
+export type { ActionBarItemProps } from './action-bar-item-wrapper.js';

+ 1 - 0
packages/dashboard/src/lib/index.ts

@@ -247,6 +247,7 @@ export * from './framework/form-engine/utils.js';
 export * from './framework/form-engine/value-transformers.js';
 export * from './framework/history-entry/history-entry-extensions.js';
 export * from './framework/history-entry/history-entry.js';
+export * from './framework/layout-engine/action-bar-item-wrapper.js';
 export * from './framework/layout-engine/dev-mode-button.js';
 export * from './framework/layout-engine/layout-extensions.js';
 export * from './framework/layout-engine/location-wrapper.js';

+ 7 - 2
packages/dev-server/test-plugins/reviews/dashboard/index.tsx

@@ -40,19 +40,24 @@ defineDashboardExtension({
     actionBarItems: [
         {
             pageId: 'product-detail',
-            component: props => {
+            component: function TestButton(props) {
                 const page = usePage();
+
                 return (
                     <Button
                         type="button"
                         onClick={() => {
-                            console.log('Clicked custom action bar item');
+                            console.log('Clicked custom action bar item', props.context.entity);
                         }}
                     >
                         Test Button
                     </Button>
                 );
             },
+            position: {
+                itemId: 'save-button',
+                order: 'after',
+            },
         },
     ],
     pageBlocks: [

+ 12 - 2
packages/dev-server/test-plugins/reviews/dashboard/review-list.tsx

@@ -1,6 +1,12 @@
 import { graphql } from '@/graphql/graphql';
 import { Trans } from '@lingui/react/macro';
-import { DashboardRouteDefinition, DetailPageButton, ListPage } from '@vendure/dashboard';
+import {
+    ActionBarItem,
+    Button,
+    DashboardRouteDefinition,
+    DetailPageButton,
+    ListPage,
+} from '@vendure/dashboard';
 
 const getReviewList = graphql(`
     query GetProductReviews($options: ProductReviewListOptions) {
@@ -83,6 +89,10 @@ export const reviewList: DashboardRouteDefinition = {
                     },
                 },
             }}
-        />
+        >
+            <ActionBarItem itemId="my-custom-button">
+                <Button>My Button</Button>
+            </ActionBarItem>
+        </ListPage>
     ),
 };