원클릭으로
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?