feat: unify sidebar panel header layout with SidebarTopArea component (#9740)

## Summary

Unify the search bar + action buttons layout across all left sidebar
panels (Node Library, Workflows, Model Library, Media Assets) using a
shared `SidebarTopArea` presentation component.

## Changes

- **What**:
- Add `SidebarTopArea.vue` — layout component with `flex-1` default slot
(search) and `#actions` slot (buttons), plus optional `bottomDivider`
prop
- Replace raw `<button>` elements in Node Library with `<Button
variant="secondary" size="icon">`
- Replace reka-ui `TabsTrigger` with shared `Tab/TabList` component in
Node Library
- Move Media Assets tab list from hover-only `#tool-buttons` to
always-visible header below search area
- Unify spacing (`gap-2`, `p-2 2xl:px-4`) and divider styles across all
sidebar panels
- Remove unused `assetType` prop and header from
`AssetsSidebarGridView`/`AssetsSidebarListView`

## Review Focus

- `SidebarTopArea` API simplicity — just slots + one optional prop
- Node Library still requires `TabsRoot` in the body for reka-ui
`TabsContent` in child panels
- Media Assets tabs are now always visible instead of hover-only

[screen-capture
(1).webm](https://github.com/user-attachments/assets/fe1d8f7b-5674-4bb3-9842-569e4c3af6c9)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9740-feat-unify-sidebar-panel-header-layout-with-SidebarTopArea-component-3206d73d365081ea8ba7fd6ac54e0169)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Jin Yi
2026-03-13 22:32:18 +09:00
committed by GitHub
parent 5167a511ce
commit 10b0350d01
19 changed files with 226 additions and 218 deletions

View File

@@ -1,22 +1,23 @@
<template>
<SidebarTabTemplate :title="$t('sideToolbar.nodes')">
<template #header>
<TabsRoot v-model="selectedTab" class="flex flex-col">
<div class="flex items-center justify-between gap-2 px-2 pb-2 2xl:px-4">
<SearchInput
ref="searchBoxRef"
v-model="searchQuery"
:placeholder="$t('g.search') + '...'"
@search="handleSearch"
/>
<SidebarTopArea bottom-divider>
<SearchInput
ref="searchBoxRef"
v-model="searchQuery"
:placeholder="$t('g.search') + '...'"
@search="handleSearch"
/>
<template #actions>
<DropdownMenuRoot>
<DropdownMenuTrigger as-child>
<button
<Button
variant="secondary"
size="icon"
:aria-label="$t('g.sort')"
class="hover:bg-comfy-input-hover flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-lg border-none bg-comfy-input"
>
<i class="icon-[lucide--arrow-up-down] size-4" />
</button>
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
@@ -42,12 +43,13 @@
</DropdownMenuRoot>
<DropdownMenuRoot v-if="selectedTab === 'all'">
<DropdownMenuTrigger as-child>
<button
<Button
variant="secondary"
size="icon"
:aria-label="$t('sideToolbar.nodeLibraryTab.filter')"
class="hover:bg-comfy-input-hover flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-lg border-none bg-comfy-input"
>
<i class="icon-[lucide--list-filter] size-4" />
</button>
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
@@ -102,65 +104,55 @@
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenuRoot>
</div>
<Separator decorative class="border border-dashed border-comfy-input" />
<!-- Tab list in header (fixed) -->
<TabsList
class="bg-background flex gap-4 border-b border-comfy-input p-4"
>
<TabsTrigger
v-for="tab in tabs"
:key="tab.value"
:value="tab.value"
:class="
cn(
'cursor-pointer rounded-lg border-none px-3 py-2 outline-none select-none',
'text-foreground text-sm transition-colors',
selectedTab === tab.value
? 'bg-comfy-input font-bold'
: 'bg-transparent font-normal'
)
"
>
</template>
</SidebarTopArea>
<div class="border-b border-comfy-input p-2 2xl:px-4">
<TabList v-model="selectedTab">
<Tab v-for="tab in tabs" :key="tab.value" :value="tab.value">
{{ tab.label }}
</TabsTrigger>
</TabsList>
</TabsRoot>
</Tab>
</TabList>
</div>
</template>
<template #body>
<NodeDragPreview />
<!-- Tab content (scrollable) -->
<TabsRoot v-model="selectedTab" class="h-full">
<EssentialNodesPanel
v-if="
flags.nodeLibraryEssentialsEnabled && selectedTab === 'essentials'
"
v-model:expanded-keys="expandedKeys"
:root="renderedEssentialRoot"
:flat-nodes="essentialFlatNodes"
@node-click="handleNodeClick"
/>
<AllNodesPanel
v-if="selectedTab === 'all'"
v-model:expanded-keys="expandedKeys"
:sections="renderedSections"
:fill-node-info="fillNodeInfo"
:sort-order="sortOrder"
@node-click="handleNodeClick"
/>
<BlueprintsPanel
v-if="selectedTab === 'blueprints'"
v-model:expanded-keys="expandedKeys"
:sections="renderedBlueprintsSections"
@node-click="handleNodeClick"
/>
</TabsRoot>
<div class="flex h-full flex-col">
<div class="min-h-0 flex-1 overflow-y-auto py-2">
<TabPanel
v-if="flags.nodeLibraryEssentialsEnabled"
:model-value="selectedTab"
value="essentials"
>
<EssentialNodesPanel
v-model:expanded-keys="expandedKeys"
:root="renderedEssentialRoot"
:flat-nodes="essentialFlatNodes"
@node-click="handleNodeClick"
/>
</TabPanel>
<TabPanel :model-value="selectedTab" value="all">
<AllNodesPanel
v-model:expanded-keys="expandedKeys"
:sections="renderedSections"
:fill-node-info="fillNodeInfo"
:sort-order="sortOrder"
@node-click="handleNodeClick"
/>
</TabPanel>
<TabPanel :model-value="selectedTab" value="blueprints">
<BlueprintsPanel
v-model:expanded-keys="expandedKeys"
:sections="renderedBlueprintsSections"
@node-click="handleNodeClick"
/>
</TabPanel>
</div>
</div>
</template>
</SidebarTabTemplate>
</template>
<script setup lang="ts">
import { cn } from '@/utils/tailwindUtil'
import { useLocalStorage } from '@vueuse/core'
import {
DropdownMenuCheckboxItem,
@@ -170,17 +162,18 @@ import {
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuRoot,
DropdownMenuTrigger,
Separator,
TabsList,
TabsRoot,
TabsTrigger
DropdownMenuTrigger
} from 'reka-ui'
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import { resolveEssentialsDisplayName } from '@/constants/essentialsDisplayNames'
import Tab from '@/components/tab/Tab.vue'
import TabList from '@/components/tab/TabList.vue'
import TabPanel from '@/components/tab/TabPanel.vue'
import SearchInput from '@/components/ui/search-input/SearchInput.vue'
import Button from '@/components/ui/button/Button.vue'
import SidebarTopArea from '@/components/sidebar/tabs/SidebarTopArea.vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useNodeDragToCanvas } from '@/composables/node/useNodeDragToCanvas'
import { usePerTabState } from '@/composables/usePerTabState'