一键导入
right-panel
Add, modify, or troubleshoot the right sidebar panel on admin pages
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Add, modify, or troubleshoot the right sidebar panel on admin pages
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Create or update user-facing documentation for Sayr features in apps/marketing
Work with the Sayr edition system (cloud, community, enterprise) including limits, capabilities, and edition-aware code
Add, remove, or modify commands and sub-views in the Cmd+K command palette
Add or modify PageHeader layouts on admin pages, including task view integration and cross-org patterns
Update GitHub pull request title and description with comprehensive summary
基于 SOC 职业分类
| name | right-panel |
| description | Add, modify, or troubleshoot the right sidebar panel on admin pages |
| metadata | {"audience":"developers","workflow":"feature-development"} |
I help you add, modify, or troubleshoot the right sidebar panel on admin pages. This includes creating panel header and body components, integrating them with the PanelWrapper layout component, and managing open/close state.
The right panel uses a props-based pattern: each page creates its own panel header and body components and passes them directly to PanelWrapper as props. There is no global store, no registration system, and no cross-route shared state. Each route fully owns its own panel content.
panels/tasks.tsx) exports a header component and a body componentpanelHeader and panelBody props to <PanelWrapper>PanelWrapper renders the header in a fixed h-11 border-b zone and the body in a scrollable area belowpanelHeader nor panelBody is provided, PanelWrapper renders just its children (no resizable group)| File | Purpose |
|---|---|
apps/start/src/components/generic/wrapper.tsx | PanelWrapper component (resizable panel layout + mobile Sheet) |
| File | Route | Content |
|---|---|---|
apps/start/src/components/admin/panels/tasks.tsx | /$orgId/tasks (tasks list) | Overview tiles, saved views/releases/priority tabs, view editing Sheet |
apps/start/src/components/admin/panels/task.tsx | /$orgId/tasks/$taskShortId (task detail) | Task metadata sidebar using TaskContentSideContent |
| Page file | Panel components used |
|---|---|
apps/start/src/components/pages/admin/orgid/tasks/index.tsx | TasksPanelHeader, TasksPanelContent |
apps/start/src/components/pages/admin/orgid/tasks/taskId.tsx | TaskPanelHeader, TaskPanelContent |
PanelWrapper is the layout component that pages opt into. It wraps the entire page content including PageHeader so the panel column sits beside everything at full height.
import { PanelWrapper } from "@/components/generic/wrapper";
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | Required | Main page content (left side) |
isOpen | boolean | Required | Whether the panel is open |
setOpen | (open: boolean) => void | Required | Toggle callback |
panelHeader | ReactNode | — | Fixed header rendered at the top of the panel (h-11, border-b) |
panelBody | ReactNode | — | Body content rendered inside the scrollable panel area |
panelDefaultSize | number | 30 | Default panel width (percentage) |
panelMinSize | number | 15 | Minimum panel width (percentage) |
className | string | — | Classes on ResizablePanelGroup root |
contentClassName | string | — | Classes on the main (left) content panel |
ResizablePanelGroup with the page content on the left and panel on the right, separated by a ResizableHandleSheet overlay for the panelpanelHeader nor panelBody is provided, returns just {children} with no wrapping<SidebarContext.Provider value={{ id: "panel-right", isCollapsed: false }}> so doras-ui SidebarMenu* components work inside the panel"panel-right" entry in the sidebar store on mountWhen panel content is provided, PanelWrapper renders:
panelHeader is provided): h-11 shrink-0 border-b px-3 — height-matched to PageHeader.IdentitypanelBody is provided): flex flex-col gap-2 flex-1 overflow-y-auto p-2 — scrollableCreate apps/start/src/components/admin/panels/<your-panel>.tsx:
"use client";
// Optional: Fixed header component, height-matched to PageHeader.Identity
export function MyPanelHeader() {
return (
<div className="flex items-center gap-2 w-full flex-1 min-w-0">
{/* Avatar, title, action buttons, etc. */}
<span className="text-xs font-medium truncate">Panel Title</span>
</div>
);
}
// Interactive content component (manages its own state, uses context hooks)
export function MyPanelContent() {
// Use context hooks, state, click handlers, etc.
return <div>Panel content here</div>;
}
In the page component:
import { PanelWrapper } from "@/components/generic/wrapper";
import { PageHeader } from "@/components/generic/PageHeader";
import { MyPanelHeader, MyPanelContent } from "@/components/admin/panels/my-panel";
import { useLayoutOrganization } from "@/contexts/ContextOrg";
export default function MyPage() {
const { isProjectPanelOpen, setProjectPanelOpen } = useLayoutOrganization();
return (
<PanelWrapper
isOpen={isProjectPanelOpen}
setOpen={setProjectPanelOpen}
panelHeader={<MyPanelHeader />}
panelBody={<MyPanelContent />}
>
<div className="relative flex flex-col h-full max-h-full">
<PageHeader>
<PageHeader.Identity ... />
<PageHeader.Toolbar
right={
<>
{/* Panel toggle button */}
<Button
variant="accent"
className="gap-2 h-6 w-fit bg-accent border-transparent p-1"
onClick={() => setProjectPanelOpen(!isProjectPanelOpen)}
>
{isProjectPanelOpen ? (
<IconLayoutSidebarRightFilled />
) : (
<IconLayoutSidebarRight />
)}
</Button>
</>
}
/>
</PageHeader>
<div className="flex-1 overflow-y-auto h-full flex flex-col relative">
{/* Page content */}
</div>
</div>
</PanelWrapper>
);
}
Critical: PanelWrapper must wrap the entire page content including PageHeader. The panel column sits beside everything at full height, not below the header.
For panels where users click tiles/tabs to trigger actions (like filtering), the component manages its own useState and calls shared hooks like useTaskViewManager.
export function TasksPanelContent() {
const [editingView, setEditingView] = useState(null);
const { applyFilter, selectView, clearView } = useTaskViewManager(views);
return (
<div className="flex flex-col gap-3 w-full">
{/* Clickable tiles */}
<Tile onClick={() => clearView()}>...</Tile>
{/* Tabs with views/releases/priority */}
<Tabs>...</Tabs>
{/* Editing Sheet */}
<Sheet open={!!editingView} onOpenChange={...}>...</Sheet>
</div>
);
}
For panels that display structured data, compose existing components. The TaskPanelContent delegates to TaskContentSideContent, pulling data from context hooks.
export function TaskPanelContent() {
const { task, setTask } = useLayoutTask();
const { organization, labels, categories, releases } = useLayoutOrganization();
const { tasks, setTasks } = useLayoutTasks();
return (
<TaskContentSideContent
task={task}
labels={labels}
tasks={tasks}
setTasks={setTasks}
setSelectedTask={(t) => t && setTask(t)}
availableUsers={organization?.members.map((m) => m.user) || []}
// ...other props
/>
);
}
SidebarMenu / SidebarMenuItem / SidebarMenuButton from doras-ui work inside the panel because PanelWrapper provides the required SidebarContext.
The panel open/close state is managed externally (not by the panel system itself). Currently, the tasks pages use isProjectPanelOpen / setProjectPanelOpen from useLayoutOrganization(), which is persisted via useStateManagement("isProjectPanelOpen", true, 30000).
When adding a panel to a new page:
isProjectPanelOpen if the page is within an org contextuseStateManagement if it should be independentisOpen / setOpen props to PanelWrapperPanel components are rendered inside the page component tree, so they have access to all context providers that wrap the page. For task pages:
useLayoutData() — account, organizations, wsuseLayoutOrganization() — organization, labels, views, categories, releases, isProjectPanelOpen, setProjectPanelOpenuseLayoutTasks() — tasks, setTasksuseLayoutTask() — task, setTask (only on /$orgId/tasks/$taskShortId)Since panel components are passed as JSX props (<MyPanelContent />), they are instantiated in the page component's render, which means they inherit all context from the page's provider tree. No special mounting considerations are needed.
h-11 border-b — height-matched to PageHeader.Identity for horizontal alignmentpanelHeader and panelBody, not a registration systemPanelWrapper provides SidebarContextIconLayoutSidebarRight / IconLayoutSidebarRightFilled iconspanels/*.tsxUse this skill when:
Tell me:
isProjectPanelOpen or new independent state?