Add globalToolbarButton slot type and update related documentation
This commit is contained in:
@@ -108,6 +108,7 @@ Mount surfaces currently wired in the host include:
|
|||||||
- `detailTab`
|
- `detailTab`
|
||||||
- `taskDetailView`
|
- `taskDetailView`
|
||||||
- `projectSidebarItem`
|
- `projectSidebarItem`
|
||||||
|
- `globalToolbarButton`
|
||||||
- `toolbarButton`
|
- `toolbarButton`
|
||||||
- `contextMenuItem`
|
- `contextMenuItem`
|
||||||
- `commentAnnotation`
|
- `commentAnnotation`
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ The same set of values is used as **slot types** (where a component mounts) and
|
|||||||
| `sidebarPanel` | Global | — |
|
| `sidebarPanel` | Global | — |
|
||||||
| `settingsPage` | Global | — |
|
| `settingsPage` | Global | — |
|
||||||
| `dashboardWidget` | Global | — |
|
| `dashboardWidget` | Global | — |
|
||||||
|
| `globalToolbarButton` | Global | — |
|
||||||
| `detailTab` | Entity | `project`, `issue`, `agent`, `goal`, `run` |
|
| `detailTab` | Entity | `project`, `issue`, `agent`, `goal`, `run` |
|
||||||
| `taskDetailView` | Entity | (task/issue context) |
|
| `taskDetailView` | Entity | (task/issue context) |
|
||||||
| `commentAnnotation` | Entity | `comment` |
|
| `commentAnnotation` | Entity | `comment` |
|
||||||
@@ -253,9 +254,13 @@ A specialized slot rendered in the context of a task or issue detail view. Simil
|
|||||||
|
|
||||||
A link or small component rendered **once per project** under that project's row in the sidebar Projects list. Use this to add project-scoped navigation entries (e.g. "Files", "Linear Sync") that deep-link into a plugin detail tab: `/:company/projects/:projectRef?tab=plugin:<key>:<slotId>`. Receives `PluginProjectSidebarItemProps` with `context.companyId` set to the active company, `context.entityId` set to the project id, and `context.entityType` set to `"project"`. Use the optional `order` field in the manifest slot to control sort position. Requires the `ui.sidebar.register` capability.
|
A link or small component rendered **once per project** under that project's row in the sidebar Projects list. Use this to add project-scoped navigation entries (e.g. "Files", "Linear Sync") that deep-link into a plugin detail tab: `/:company/projects/:projectRef?tab=plugin:<key>:<slotId>`. Receives `PluginProjectSidebarItemProps` with `context.companyId` set to the active company, `context.entityId` set to the project id, and `context.entityType` set to `"project"`. Use the optional `order` field in the manifest slot to control sort position. Requires the `ui.sidebar.register` capability.
|
||||||
|
|
||||||
|
#### `globalToolbarButton`
|
||||||
|
|
||||||
|
A button rendered in the global top bar (breadcrumb bar) that appears on every page. Use this for company-wide actions that are not scoped to a specific entity — for example, a universal search trigger, a global sync status indicator, or a floating action that applies across the whole workspace. Receives only `context.companyId` and `context.companyPrefix`; no entity context is available. Requires the `ui.action.register` capability.
|
||||||
|
|
||||||
#### `toolbarButton`
|
#### `toolbarButton`
|
||||||
|
|
||||||
A button rendered in the toolbar of a host surface (e.g. project detail, issue detail). Use this for short-lived, contextual actions like triggering a sync, opening a picker, or running a quick command. The component can open a plugin-owned modal internally for confirmations or compact forms. Receives `context.companyId` set to the active company; entity context varies by host surface. Requires the `ui.action.register` capability.
|
A button rendered in the toolbar of an entity page (e.g. project detail, issue detail). Use this for short-lived, contextual actions scoped to the current entity — like triggering a project sync, opening a picker, or running a quick command on that entity. The component can open a plugin-owned modal internally for confirmations or compact forms. Receives `context.companyId`, `context.entityId`, and `context.entityType`; declare `entityTypes` in the manifest to control which entity pages the button appears on. Requires the `ui.action.register` capability.
|
||||||
|
|
||||||
#### `contextMenuItem`
|
#### `contextMenuItem`
|
||||||
|
|
||||||
@@ -481,7 +486,9 @@ Each slot type receives a typed props object with `context: PluginHostContext`.
|
|||||||
| `sidebar` | `PluginSidebarProps` | — |
|
| `sidebar` | `PluginSidebarProps` | — |
|
||||||
| `settingsPage` | `PluginSettingsPageProps` | — |
|
| `settingsPage` | `PluginSettingsPageProps` | — |
|
||||||
| `dashboardWidget` | `PluginWidgetProps` | — |
|
| `dashboardWidget` | `PluginWidgetProps` | — |
|
||||||
|
| `globalToolbarButton` | `PluginGlobalToolbarButtonProps` | — |
|
||||||
| `detailTab` | `PluginDetailTabProps` | `entityId: string`, `entityType: string` |
|
| `detailTab` | `PluginDetailTabProps` | `entityId: string`, `entityType: string` |
|
||||||
|
| `toolbarButton` | `PluginToolbarButtonProps` | `entityId: string`, `entityType: string` |
|
||||||
| `commentAnnotation` | `PluginCommentAnnotationProps` | `entityId: string`, `entityType: "comment"`, `parentEntityId: string`, `projectId`, `companyPrefix` |
|
| `commentAnnotation` | `PluginCommentAnnotationProps` | `entityId: string`, `entityType: "comment"`, `parentEntityId: string`, `projectId`, `companyPrefix` |
|
||||||
| `commentContextMenuItem` | `PluginCommentContextMenuItemProps` | `entityId: string`, `entityType: "comment"`, `parentEntityId: string`, `projectId`, `companyPrefix` |
|
| `commentContextMenuItem` | `PluginCommentContextMenuItemProps` | `entityId: string`, `entityType: "comment"`, `parentEntityId: string`, `projectId`, `companyPrefix` |
|
||||||
| `projectSidebarItem` | `PluginProjectSidebarItemProps` | `entityId: string`, `entityType: "project"` |
|
| `projectSidebarItem` | `PluginProjectSidebarItemProps` | `entityId: string`, `entityType: "project"` |
|
||||||
@@ -521,7 +528,7 @@ V1 does not provide a dedicated `modal` slot. Plugins can either:
|
|||||||
- declare concrete UI mount points in `ui.slots`
|
- declare concrete UI mount points in `ui.slots`
|
||||||
- declare host-rendered entry points in `ui.launchers`
|
- declare host-rendered entry points in `ui.launchers`
|
||||||
|
|
||||||
Supported launcher placement zones currently mirror the major host surfaces such as `projectSidebarItem`, `toolbarButton`, `detailTab`, `settingsPage`, and `contextMenuItem`. Plugins may still open their own local modal from those entry points when needed.
|
Supported launcher placement zones currently mirror the major host surfaces such as `projectSidebarItem`, `globalToolbarButton`, `toolbarButton`, `detailTab`, `settingsPage`, and `contextMenuItem`. Plugins may still open their own local modal from those entry points when needed.
|
||||||
|
|
||||||
Declarative launcher example:
|
Declarative launcher example:
|
||||||
|
|
||||||
@@ -597,7 +604,14 @@ Use optional `order` in the slot to sort among other project sidebar items. See
|
|||||||
|
|
||||||
## Toolbar launcher with a local modal
|
## Toolbar launcher with a local modal
|
||||||
|
|
||||||
For short-lived actions, mount a `toolbarButton` and open a plugin-owned modal inside the component. Use `useHostContext()` to scope the action to the current company or project.
|
Two toolbar slot types are available depending on where the button should appear:
|
||||||
|
|
||||||
|
- **`globalToolbarButton`** — renders in the top bar on every page, scoped to the company. No entity context. Use for workspace-wide actions.
|
||||||
|
- **`toolbarButton`** — renders on entity detail pages (project, issue, etc.). Receives `entityId` and `entityType`. Declare `entityTypes` to control which pages the button appears on.
|
||||||
|
|
||||||
|
For short-lived actions, mount the appropriate slot type and open a plugin-owned modal inside the component. Use `useHostContext()` to scope the action to the current company or entity.
|
||||||
|
|
||||||
|
Project-scoped example (appears only on project detail pages):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -607,7 +621,8 @@ For short-lived actions, mount a `toolbarButton` and open a plugin-owned modal i
|
|||||||
"type": "toolbarButton",
|
"type": "toolbarButton",
|
||||||
"id": "sync-toolbar-button",
|
"id": "sync-toolbar-button",
|
||||||
"displayName": "Sync",
|
"displayName": "Sync",
|
||||||
"exportName": "SyncToolbarButton"
|
"exportName": "SyncToolbarButton",
|
||||||
|
"entityTypes": ["project"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -370,6 +370,7 @@ export const PLUGIN_UI_SLOT_TYPES = [
|
|||||||
"sidebar",
|
"sidebar",
|
||||||
"sidebarPanel",
|
"sidebarPanel",
|
||||||
"projectSidebarItem",
|
"projectSidebarItem",
|
||||||
|
"globalToolbarButton",
|
||||||
"toolbarButton",
|
"toolbarButton",
|
||||||
"contextMenuItem",
|
"contextMenuItem",
|
||||||
"commentAnnotation",
|
"commentAnnotation",
|
||||||
@@ -419,6 +420,7 @@ export const PLUGIN_LAUNCHER_PLACEMENT_ZONES = [
|
|||||||
"sidebar",
|
"sidebar",
|
||||||
"sidebarPanel",
|
"sidebarPanel",
|
||||||
"projectSidebarItem",
|
"projectSidebarItem",
|
||||||
|
"globalToolbarButton",
|
||||||
"toolbarButton",
|
"toolbarButton",
|
||||||
"contextMenuItem",
|
"contextMenuItem",
|
||||||
"commentAnnotation",
|
"commentAnnotation",
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ const UI_SLOT_CAPABILITIES: Record<PluginUiSlotType, PluginCapability> = {
|
|||||||
detailTab: "ui.detailTab.register",
|
detailTab: "ui.detailTab.register",
|
||||||
taskDetailView: "ui.detailTab.register",
|
taskDetailView: "ui.detailTab.register",
|
||||||
dashboardWidget: "ui.dashboardWidget.register",
|
dashboardWidget: "ui.dashboardWidget.register",
|
||||||
|
globalToolbarButton: "ui.action.register",
|
||||||
toolbarButton: "ui.action.register",
|
toolbarButton: "ui.action.register",
|
||||||
contextMenuItem: "ui.action.register",
|
contextMenuItem: "ui.action.register",
|
||||||
commentAnnotation: "ui.commentAnnotation.register",
|
commentAnnotation: "ui.commentAnnotation.register",
|
||||||
@@ -124,6 +125,7 @@ const LAUNCHER_PLACEMENT_CAPABILITIES: Record<
|
|||||||
sidebar: "ui.sidebar.register",
|
sidebar: "ui.sidebar.register",
|
||||||
sidebarPanel: "ui.sidebar.register",
|
sidebarPanel: "ui.sidebar.register",
|
||||||
projectSidebarItem: "ui.sidebar.register",
|
projectSidebarItem: "ui.sidebar.register",
|
||||||
|
globalToolbarButton: "ui.action.register",
|
||||||
toolbarButton: "ui.action.register",
|
toolbarButton: "ui.action.register",
|
||||||
contextMenuItem: "ui.action.register",
|
contextMenuItem: "ui.action.register",
|
||||||
commentAnnotation: "ui.commentAnnotation.register",
|
commentAnnotation: "ui.commentAnnotation.register",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { Fragment, useMemo } from "react";
|
import { Fragment, useMemo } from "react";
|
||||||
import { PluginSlotOutlet } from "@/plugins/slots";
|
import { PluginSlotOutlet } from "@/plugins/slots";
|
||||||
|
import { PluginLauncherOutlet } from "@/plugins/launchers";
|
||||||
|
|
||||||
export function BreadcrumbBar() {
|
export function BreadcrumbBar() {
|
||||||
const { breadcrumbs } = useBreadcrumbs();
|
const { breadcrumbs } = useBreadcrumbs();
|
||||||
@@ -29,11 +30,18 @@ export function BreadcrumbBar() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const globalToolbarSlots = (
|
const globalToolbarSlots = (
|
||||||
<PluginSlotOutlet
|
<div className="flex items-center gap-1 ml-auto shrink-0 pl-2">
|
||||||
slotTypes={["toolbarButton"]}
|
<PluginSlotOutlet
|
||||||
context={globalToolbarSlotContext}
|
slotTypes={["globalToolbarButton"]}
|
||||||
className="flex items-center gap-1 ml-auto shrink-0 pl-2"
|
context={globalToolbarSlotContext}
|
||||||
/>
|
className="flex items-center gap-1"
|
||||||
|
/>
|
||||||
|
<PluginLauncherOutlet
|
||||||
|
placementZones={["globalToolbarButton"]}
|
||||||
|
context={globalToolbarSlotContext}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (breadcrumbs.length === 0) {
|
if (breadcrumbs.length === 0) {
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ const entityScopedZones = new Set<PluginLauncherPlacementZone>([
|
|||||||
"commentAnnotation",
|
"commentAnnotation",
|
||||||
"commentContextMenuItem",
|
"commentContextMenuItem",
|
||||||
"projectSidebarItem",
|
"projectSidebarItem",
|
||||||
|
"toolbarButton",
|
||||||
]);
|
]);
|
||||||
const focusableElementSelector = [
|
const focusableElementSelector = [
|
||||||
"button:not([disabled])",
|
"button:not([disabled])",
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ function buildRegistryKey(pluginKey: string, exportName: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function requiresEntityType(slotType: PluginUiSlotType): boolean {
|
function requiresEntityType(slotType: PluginUiSlotType): boolean {
|
||||||
return slotType === "detailTab" || slotType === "taskDetailView" || slotType === "contextMenuItem" || slotType === "commentAnnotation" || slotType === "commentContextMenuItem" || slotType === "projectSidebarItem";
|
return slotType === "detailTab" || slotType === "taskDetailView" || slotType === "contextMenuItem" || slotType === "commentAnnotation" || slotType === "commentContextMenuItem" || slotType === "projectSidebarItem" || slotType === "toolbarButton";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getErrorMessage(error: unknown): string {
|
function getErrorMessage(error: unknown): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user