mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-02 12:11:58 +00:00
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:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user