mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## 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>
75 lines
2.1 KiB
Vue
75 lines
2.1 KiB
Vue
<template>
|
|
<button
|
|
:id="`tab-${props.value}`"
|
|
role="tab"
|
|
type="button"
|
|
:aria-selected="isActive"
|
|
:aria-controls="`tabpanel-${props.value}`"
|
|
:data-state="isActive ? 'active' : 'inactive'"
|
|
:tabindex="isActive ? 0 : -1"
|
|
:class="
|
|
cn(
|
|
'flex shrink-0 items-center justify-center',
|
|
'cursor-pointer rounded-lg border-none px-2.5 py-2 text-sm transition-all duration-200',
|
|
'focus-visible:ring-ring/20 outline-hidden focus-visible:ring-1',
|
|
isActive
|
|
? 'bg-interface-menu-component-surface-hovered text-text-primary'
|
|
: 'bg-transparent text-text-secondary hover:bg-button-hover-surface focus:bg-button-hover-surface',
|
|
props.class
|
|
)
|
|
"
|
|
@click="handleClick"
|
|
@keydown="handleKeydown"
|
|
>
|
|
<slot />
|
|
</button>
|
|
</template>
|
|
|
|
<script setup lang="ts" generic="T extends string = string">
|
|
import type { HTMLAttributes } from 'vue'
|
|
|
|
import { computed, inject } from 'vue'
|
|
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
|
|
import { TAB_LIST_INJECTION_KEY } from './tabKeys'
|
|
|
|
const props = defineProps<{
|
|
value: T
|
|
class?: HTMLAttributes['class']
|
|
}>()
|
|
|
|
const context = inject(TAB_LIST_INJECTION_KEY)
|
|
|
|
const isActive = computed(() => context?.modelValue.value === props.value)
|
|
|
|
function handleClick() {
|
|
context?.select(props.value)
|
|
}
|
|
|
|
function handleKeydown(event: KeyboardEvent) {
|
|
const tablist = (event.currentTarget as HTMLElement).parentElement
|
|
if (!tablist) return
|
|
|
|
const tabs = Array.from(tablist.querySelectorAll<HTMLElement>('[role="tab"]'))
|
|
const currentIndex = tabs.indexOf(event.currentTarget as HTMLElement)
|
|
|
|
let targetIndex = -1
|
|
|
|
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
|
|
targetIndex = (currentIndex + 1) % tabs.length
|
|
} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
|
|
targetIndex = (currentIndex - 1 + tabs.length) % tabs.length
|
|
} else if (event.key === 'Home') {
|
|
targetIndex = 0
|
|
} else if (event.key === 'End') {
|
|
targetIndex = tabs.length - 1
|
|
}
|
|
|
|
if (targetIndex !== -1) {
|
|
event.preventDefault()
|
|
tabs[targetIndex].focus()
|
|
}
|
|
}
|
|
</script>
|