Commit 468ea511 authored by edy's avatar edy

feat(ui): 左侧导航栏移植Windows客户端透明风格UI,新增搜索过滤功能

- 侧边栏背景/导航按钮/专家分类/专家条目/会话卡片改为透明若隐若现风格
- 新增侧边栏搜索框,支持过滤导航项、专家列表、会话列表
- 去掉品牌名千匠问天四字,仅保留logo图标
- 清理.sidebar-brand-name死代码和重复的.sidebar-filter-empty样式
- 修复sidebarKnowledgeSource测试用例以适配新代码结构
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 0d0a22ea
Pipeline #18503 failed
import type { ReactNode } from "react"
import { useMemo, useState, type ReactNode } from "react"
import type { ProjectSessionSummary } from "@qjclaw/shared-types"
import { Sidebar } from "./Sidebar"
import { ExpertTree, type ExpertCategoryId, type ExpertVisualKey, type SidebarExpertEntry } from "./ExpertTree"
......@@ -69,6 +69,44 @@ export function AppSidebar({
onOpenSession,
onCloseSession
}: AppSidebarProps) {
const [sidebarSearchQuery, setSidebarSearchQuery] = useState("")
const normalizedSidebarSearchQuery = sidebarSearchQuery.trim().toLocaleLowerCase()
const isSearchingSidebar = normalizedSidebarSearchQuery.length > 0
const matchesSidebarSearch = (value: string) => value.toLocaleLowerCase().includes(normalizedSidebarSearchQuery)
const filteredSessions = useMemo(
() => {
if (!isSearchingSidebar) {
return sessions
}
if (matchesSidebarSearch(sidebarSessionLabel)) {
return sessions
}
return sessions.filter((session, index) => {
const title = sidebarSessionTitles[session.id] ?? formatSessionTitle(session.title, index)
return matchesSidebarSearch(title)
})
},
[formatSessionTitle, isSearchingSidebar, normalizedSidebarSearchQuery, sessions, sidebarSessionLabel, sidebarSessionTitles]
)
const headerContent = (
<label className="sidebar-search app-no-drag">
<span className="sidebar-search-icon" aria-hidden="true">
<svg viewBox="0 0 20 20" focusable="false">
<path d="m14.2 14.2 2.8 2.8M8.9 15.1a6.2 6.2 0 1 1 0-12.4 6.2 6.2 0 0 1 0 12.4Z" />
</svg>
</span>
<input
type="search"
value={sidebarSearchQuery}
placeholder="搜索"
aria-label="搜索侧栏"
onChange={(event) => setSidebarSearchQuery(event.target.value)}
/>
</label>
)
const topContent = (
<>
<nav className="nav-list" aria-label="主导航">
......@@ -78,7 +116,7 @@ export function AppSidebar({
{ id: "knowledge" as const, label: ui.knowledge },
{ id: "automation" as const, label: "自动化任务" },
{ id: "settings" as const, label: ui.settings }
].map((item) => (
].filter((item) => !isSearchingSidebar || matchesSidebarSearch(item.label)).map((item) => (
<button
key={item.id}
type="button"
......@@ -93,9 +131,7 @@ export function AppSidebar({
</button>
))}
</nav>
<div className="conversation-sidebar-action">
{!showBindEntry ? sidebarNewSessionAction : null}
</div>
{!showBindEntry ? <div className="conversation-sidebar-action">{sidebarNewSessionAction}</div> : null}
</>
)
......@@ -107,6 +143,7 @@ export function AppSidebar({
viewMode={viewMode}
prompt={prompt}
activeProjectId={activeProjectId}
searchQuery={normalizedSidebarSearchQuery}
onToggleCategory={onToggleCategory}
onExpertSelect={onExpertSelect}
renderCategoryIcon={renderCategoryIcon}
......@@ -115,7 +152,8 @@ export function AppSidebar({
<SessionList
visible={!showBindEntry}
label={sidebarSessionLabel}
sessions={sessions}
sessions={filteredSessions}
totalSessionCount={sessions.length}
activeSessionId={activeSessionId}
sessionTitles={sidebarSessionTitles}
projectActionPending={projectActionPending}
......@@ -123,6 +161,7 @@ export function AppSidebar({
activeStreamSessionId={activeStreamSessionId}
closeLabel={ui.closeSession}
formatSessionTitle={formatSessionTitle}
isFiltering={isSearchingSidebar}
onOpenSession={onOpenSession}
onCloseSession={onCloseSession}
/>
......@@ -133,6 +172,7 @@ export function AppSidebar({
<Sidebar
topContent={topContent}
bottomContent={bottomContent}
headerContent={headerContent}
isCollapsed={isCollapsed}
onToggleCollapsed={onToggleCollapsed}
/>
......
......@@ -107,6 +107,7 @@ interface ExpertTreeProps {
viewMode: "chat" | "experts" | "tasks" | "automation" | "plugins" | "settings" | "knowledge"
prompt: string
activeProjectId?: string
searchQuery?: string
onToggleCategory(categoryId: string): void
onExpertSelect(entry: SidebarExpertEntry): void
renderCategoryIcon(categoryId: ExpertCategoryId): ReactNode
......@@ -119,11 +120,40 @@ export function ExpertTree({
viewMode,
prompt,
activeProjectId,
searchQuery = "",
onToggleCategory,
onExpertSelect,
renderCategoryIcon,
renderExpertIcon
}: ExpertTreeProps) {
const normalizedSearchQuery = searchQuery.trim().toLocaleLowerCase()
const isSearching = normalizedSearchQuery.length > 0
const matchesSearch = (value: string) => value.toLocaleLowerCase().includes(normalizedSearchQuery)
const sectionMatches = isSearching && matchesSearch("数字员工")
const visibleCategories = CATEGORY_CONFIG.map((category) => {
const categoryEntries = entries.filter((entry) => expertMatchesCategory(entry, category.id))
const categoryMatches = sectionMatches || (isSearching && matchesSearch(category.name))
const categoryExperts = isSearching && !categoryMatches
? categoryEntries.filter((entry) => matchesSearch(entry.displayName) || matchesSearch(entry.definition.id))
: categoryEntries
const hasMatchingExperts = categoryExperts.length > 0
if (isSearching && !categoryMatches && !hasMatchingExperts) {
return null
}
return {
category,
categoryExperts,
hasExperts: categoryEntries.length > 0,
isExpanded: isSearching ? hasMatchingExperts : expandedCategories[category.id] || false
}
}).filter((categoryModel): categoryModel is NonNullable<typeof categoryModel> => categoryModel !== null)
if (isSearching && visibleCategories.length === 0) {
return null
}
return (
<section className="sidebar-section compact sidebar-experts-entry">
<div className="sidebar-section-head sidebar-digital-workers-title">
......@@ -134,11 +164,8 @@ export function ExpertTree({
</div>
<div className="sidebar-expert-scroll">
<div className="expert-category-list">
{CATEGORY_CONFIG.map((category) => {
const categoryExperts = entries.filter((entry) => expertMatchesCategory(entry, category.id))
const isExpanded = expandedCategories[category.id] || false
{visibleCategories.map(({ category, categoryExperts, hasExperts, isExpanded }) => {
const categoryPanelId = `expert-tree-${category.id}`
const hasExperts = categoryExperts.length > 0
return (
<div key={category.id} className={"expert-category-item expert-tree-category" + (isExpanded && hasExperts ? " expanded" : "")}>
......
......@@ -6,6 +6,7 @@ interface SessionListProps {
visible: boolean
label: string
sessions: ProjectSessionSummary[]
totalSessionCount?: number
activeSessionId: string
sessionTitles: Record<string, string>
projectActionPending: boolean
......@@ -13,6 +14,7 @@ interface SessionListProps {
activeStreamSessionId?: string
closeLabel: string
formatSessionTitle(title: string, index: number): string
isFiltering?: boolean
onOpenSession(session: ProjectSessionSummary): void
onCloseSession(sessionId: string): void
}
......@@ -21,6 +23,7 @@ export function SessionList({
visible,
label,
sessions,
totalSessionCount,
activeSessionId,
sessionTitles,
projectActionPending,
......@@ -28,6 +31,7 @@ export function SessionList({
activeStreamSessionId,
closeLabel,
formatSessionTitle,
isFiltering = false,
onOpenSession,
onCloseSession
}: SessionListProps) {
......@@ -35,6 +39,8 @@ export function SessionList({
return null
}
const effectiveTotal = totalSessionCount ?? sessions.length
return (
<section className="sidebar-section sidebar-section-fill compact sidebar-session-section">
<div className="sidebar-section-head sidebar-section-head-subtle">
......@@ -44,33 +50,37 @@ export function SessionList({
</div>
</div>
<div className="sidebar-session-list">
{sessions.map((session, index) => (
<div key={session.id} className={"sidebar-session-card" + (activeSessionId === session.id ? " active" : "")}>
<button
type="button"
className="sidebar-session-main app-no-drag"
aria-current={activeSessionId === session.id ? "true" : undefined}
disabled={projectActionPending}
onClick={() => onOpenSession(session)}
>
<strong>{sessionTitles[session.id] ?? formatSessionTitle(session.title, index)}{session.isChannelSession ? <span className="sidebar-session-tag">飞书</span> : null}</strong>
</button>
{sessions.length > 1 ? (
{sessions.length > 0 ? (
sessions.map((session, index) => (
<div key={session.id} className={"sidebar-session-card" + (activeSessionId === session.id ? " active" : "")}>
<button
type="button"
className="sidebar-session-close app-no-drag"
aria-label={closeLabel}
title={closeLabel}
disabled={projectActionPending || (sendPhase !== "idle" && activeStreamSessionId === session.id)}
onClick={() => onCloseSession(session.id)}
className="sidebar-session-main app-no-drag"
aria-current={activeSessionId === session.id ? "true" : undefined}
disabled={projectActionPending}
onClick={() => onOpenSession(session)}
>
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M4.25 4.25 11.75 11.75M11.75 4.25 4.25 11.75" />
</svg>
<strong>{sessionTitles[session.id] ?? formatSessionTitle(session.title, index)}{session.isChannelSession ? <span className="sidebar-session-tag">飞书</span> : null}</strong>
</button>
) : null}
</div>
))}
{effectiveTotal > 1 ? (
<button
type="button"
className="sidebar-session-close app-no-drag"
aria-label={closeLabel}
title={closeLabel}
disabled={projectActionPending || (sendPhase !== "idle" && activeStreamSessionId === session.id)}
onClick={() => onCloseSession(session.id)}
>
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M4.25 4.25 11.75 11.75M11.75 4.25 4.25 11.75" />
</svg>
</button>
) : null}
</div>
))
) : isFiltering ? (
<div className="sidebar-filter-empty">没有匹配会话</div>
) : null}
</div>
</section>
)
......
......@@ -4,16 +4,23 @@ import brandIcon from "../../assets/brand-icon.png"
interface SidebarProps {
topContent: ReactNode
bottomContent: ReactNode
headerContent?: ReactNode
isCollapsed: boolean
onToggleCollapsed(): void
}
export function Sidebar({ topContent, bottomContent, isCollapsed, onToggleCollapsed }: SidebarProps) {
export function Sidebar({ topContent, bottomContent, headerContent, isCollapsed, onToggleCollapsed }: SidebarProps) {
return (
<aside className="sidebar conversation-sidebar-layout app-drag-region">
<div className="sidebar-brand">
<img src={brandIcon} alt="" className="sidebar-brand-logo" />
<strong className="sidebar-brand-name">千匠问天</strong>
{!isCollapsed ? (
<img src={brandIcon} alt="" className="sidebar-brand-logo" />
) : null}
{!isCollapsed && headerContent ? (
<div className="sidebar-header-content">
{headerContent}
</div>
) : null}
<button
type="button"
className="sidebar-collapse-button app-no-drag"
......
......@@ -15,6 +15,10 @@
grid-template-columns: 64px minmax(0, 1fr);
}
.shell.sidebar-collapsed {
grid-template-columns: 64px minmax(0, 1fr);
}
.skip-link {
position: fixed;
top: 10px;
......@@ -48,6 +52,19 @@
box-shadow: inset -1px 0 0 rgba(148, 163, 184, 0.22);
}
.sidebar-filter-empty {
min-height: 42px;
display: grid;
place-items: center;
padding: 0 12px;
border-radius: 10px;
border: 1px dashed rgba(96, 165, 250, 0.28);
background: rgba(255, 255, 255, 0.52);
color: #64748b;
font-size: 13px;
line-height: 1.4;
}
.nav-list,
.page-stack,
.catalog-list,
......@@ -129,7 +146,7 @@
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: rgba(96, 165, 250, 0.34) transparent;
scrollbar-color: rgba(94, 164, 216, 0.44) transparent;
}
.sidebar-expert-scroll::-webkit-scrollbar {
......@@ -138,16 +155,41 @@
.sidebar-expert-scroll::-webkit-scrollbar-track {
background: transparent;
border-radius: 2px;
}
.sidebar-expert-scroll::-webkit-scrollbar-thumb {
background: rgba(96, 165, 250, 0.34);
border-radius: 2px;
transition: background 0.2s ease;
background: rgba(94, 164, 216, 0.44);
border-radius: 999px;
}
.sidebar-expert-scroll:hover::-webkit-scrollbar-thumb,
.sidebar-expert-scroll:focus-within::-webkit-scrollbar-thumb {
background: rgba(37, 99, 235, 0.42);
}
.sidebar-session-list {
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: rgba(94, 164, 216, 0.44) transparent;
}
.sidebar-session-list::-webkit-scrollbar {
width: 4px;
}
.sidebar-session-list::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-session-list::-webkit-scrollbar-thumb {
background: rgba(94, 164, 216, 0.44);
border-radius: 999px;
}
.sidebar-expert-scroll::-webkit-scrollbar-thumb:hover {
.sidebar-session-list:hover::-webkit-scrollbar-thumb,
.sidebar-session-list:focus-within::-webkit-scrollbar-thumb {
background: rgba(37, 99, 235, 0.42);
}
......
......@@ -56,10 +56,12 @@
position: relative;
height: 100vh;
overflow: hidden;
background: linear-gradient(180deg, #eaf5ff 0%, #d5eaff 48%, #bddbfa 100%);
border-right: 1px solid rgba(37, 99, 235, 0.34);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.36), rgba(255, 255, 255, 0)),
linear-gradient(180deg, #dbeafe 0%, #c7ddf8 100%);
border-right: 1px solid rgba(37, 99, 235, 0.14);
border-bottom: 0;
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.66);
box-shadow: none;
}
.shell.openclaw-theme > .sidebar::before,
......@@ -71,11 +73,17 @@
.shell.openclaw-theme > .sidebar::before {
inset: 0;
background: linear-gradient(145deg, rgba(255, 255, 255, 0.44) 0%, rgba(255, 255, 255, 0) 34%, rgba(37, 99, 235, 0.1) 100%);
background: linear-gradient(145deg, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0) 32%, rgba(37, 99, 235, 0.06) 100%);
}
.shell.openclaw-theme > .sidebar::after {
content: none;
top: -14%;
left: -18%;
width: 72%;
height: 34%;
border-radius: 999px;
background: radial-gradient(circle, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0) 72%);
opacity: 0.5;
}
.shell.openclaw-theme .conversation-sidebar-layout {
......@@ -88,11 +96,12 @@
.shell.openclaw-theme .sidebar-brand {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 32px minmax(0, 1fr) 28px;
min-width: 0;
min-height: 42px;
display: flex;
align-items: center;
gap: 10px;
min-height: 36px;
gap: 8px;
justify-content: flex-end;
}
.shell.openclaw-theme .sidebar-brand-logo {
......@@ -102,40 +111,100 @@
object-fit: contain;
}
.shell.openclaw-theme .sidebar-brand-name {
.shell.openclaw-theme .sidebar-header-content {
min-width: 0;
overflow: hidden;
flex: 1 1 auto;
}
.shell.openclaw-theme .sidebar-search {
position: relative;
min-width: 0;
height: 34px;
display: flex;
align-items: center;
color: #41657f;
}
.shell.openclaw-theme .sidebar-search input {
width: 100%;
height: 34px;
min-width: 0;
padding: 0 32px 0 34px;
border-radius: 999px;
border: 1px solid transparent;
background: rgba(255, 255, 255, 0.4);
color: #12355f;
font-size: 15px;
font-weight: 800;
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
font: inherit;
font-size: 13px;
outline: none;
box-shadow: none;
transition: background 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
}
.shell.openclaw-theme .sidebar-search input::placeholder {
color: #6b87a0;
}
.shell.openclaw-theme .sidebar-search input:focus {
border-color: rgba(37, 99, 235, 0.4);
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.08);
}
.shell.openclaw-theme .sidebar-search-icon {
position: absolute;
z-index: 1;
display: grid;
place-items: center;
color: currentColor;
left: 11px;
width: 16px;
height: 16px;
pointer-events: none;
}
.shell.openclaw-theme .sidebar-search-icon svg {
width: 100%;
height: 100%;
display: block;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.shell.openclaw-theme .sidebar-collapse-button {
width: 28px;
height: 28px;
padding: 0;
display: inline-grid;
width: 30px;
height: 30px;
min-width: 30px;
display: grid;
place-items: center;
border: 1px solid rgba(96, 165, 250, 0.28);
border-radius: 8px;
background: rgba(255, 255, 255, 0.74);
color: #2563eb;
padding: 0;
border-radius: 9px;
border: 1px solid transparent;
background: transparent;
color: #1d4ed8;
box-shadow: none;
transition: border-color 180ms ease, background 180ms ease, color 180ms ease;
transition: background 160ms ease, border-color 160ms ease, color 160ms ease, box-shadow 160ms ease;
}
.shell.openclaw-theme .sidebar-collapse-button:hover:not(:disabled) {
border-color: rgba(37, 99, 235, 0.48);
background: #ffffff;
color: #1d4ed8;
border-color: rgba(37, 99, 235, 0.24);
background: rgba(255, 255, 255, 0.7);
color: #0f6eea;
box-shadow: none;
}
.shell.openclaw-theme .sidebar-collapse-button svg {
width: 18px;
height: 18px;
display: block;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.shell.openclaw-theme .sidebar-top {
......@@ -154,14 +223,14 @@
min-height: 42px;
padding: 0 12px 0 14px;
border-radius: 14px;
border: 1px solid rgba(96, 165, 250, 0.18);
background: rgba(255, 255, 255, 0.36);
border: 1px solid transparent;
background: transparent;
color: #2e516c;
justify-content: flex-start;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease, color 180ms ease;
transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease, color 180ms ease, transform 180ms ease;
}
.shell.openclaw-theme .nav-item-icon svg {
......@@ -174,8 +243,8 @@
}
.shell.openclaw-theme .nav-item:hover:not(.active) {
background: rgba(255, 255, 255, 0.62);
border-color: rgba(96, 165, 250, 0.42);
background: rgba(255, 255, 255, 0.54);
border-color: rgba(96, 165, 250, 0.22);
color: var(--revamp-blue-strong);
box-shadow: none;
}
......@@ -185,8 +254,8 @@
border-color: var(--revamp-blue-border);
color: var(--revamp-blue-strong);
box-shadow:
inset 4px 0 0 var(--revamp-blue),
var(--revamp-shadow-soft);
inset 3px 0 0 var(--revamp-blue),
0 4px 12px rgba(37, 99, 235, 0.08);
}
.shell.openclaw-theme .nav-item.active::before {
......@@ -200,22 +269,22 @@
.shell.openclaw-theme .sidebar-new-session.conversation-new-session {
width: 100%;
min-height: 42px;
min-height: 44px;
justify-content: center;
padding: 0 12px;
padding: 0 16px;
border-radius: 14px;
border-color: rgba(29, 78, 216, 0.18);
border-color: transparent;
background: linear-gradient(135deg, #2563eb, #0f6eea);
color: #ffffff;
box-shadow: var(--revamp-shadow-soft);
transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease, color 180ms ease;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.16);
transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease, color 180ms ease, transform 180ms ease;
}
.shell.openclaw-theme .sidebar-new-session.conversation-new-session:hover:not(:disabled) {
background: linear-gradient(135deg, #1d4ed8, #075bd8);
border-color: rgba(29, 78, 216, 0.32);
border-color: transparent;
color: #ffffff;
box-shadow: var(--revamp-shadow);
box-shadow: 0 6px 14px rgba(37, 99, 235, 0.2);
}
.shell.openclaw-theme .conversation-new-session-plus {
......@@ -246,108 +315,100 @@
padding-right: 0;
}
.shell.openclaw-theme .conversation-new-session,
.shell.openclaw-theme .sidebar-experts-entry,
.shell.openclaw-theme .sidebar-session-section,
.shell.openclaw-theme .expert-category-item,
.shell.openclaw-theme .sidebar-session-card {
border-color: var(--revamp-border);
background: rgba(255, 255, 255, 0.62);
box-shadow: none;
}
.shell.openclaw-theme .sidebar-experts-entry {
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 8px;
overflow: hidden;
padding: 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.82);
border-color: rgba(59, 130, 246, 0.16);
box-shadow: none;
}
.shell.openclaw-theme .sidebar-session-section {
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
overflow: hidden;
padding: 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.82);
border-color: rgba(59, 130, 246, 0.16);
box-shadow: none;
}
.shell.openclaw-theme.sidebar-collapsed .conversation-sidebar-layout {
grid-template-rows: auto 1fr;
gap: 14px;
justify-items: center;
gap: 12px;
padding: 16px 10px;
}
.shell.openclaw-theme.sidebar-collapsed .sidebar-brand {
grid-template-columns: 44px;
justify-items: center;
width: 44px;
justify-content: center;
gap: 0;
min-height: 44px;
}
.shell.openclaw-theme.sidebar-collapsed .sidebar-header-content,
.shell.openclaw-theme.sidebar-collapsed .sidebar-brand-logo,
.shell.openclaw-theme.sidebar-collapsed .sidebar-brand-name,
.shell.openclaw-theme.sidebar-collapsed .nav-item-label,
.shell.openclaw-theme.sidebar-collapsed .conversation-sidebar-action,
.shell.openclaw-theme.sidebar-collapsed .sidebar-bottom {
display: none;
}
.shell.openclaw-theme.sidebar-collapsed .sidebar-collapse-button {
width: 44px;
height: 44px;
border-radius: 10px;
width: 38px;
height: 38px;
min-width: 38px;
}
.shell.openclaw-theme.sidebar-collapsed .sidebar-top {
min-height: 0;
width: 44px;
align-content: start;
gap: 12px;
justify-items: center;
}
.shell.openclaw-theme.sidebar-collapsed .nav-list {
width: 44px;
justify-items: center;
align-content: start;
gap: 8px;
}
.shell.openclaw-theme.sidebar-collapsed .nav-item {
width: 44px;
min-width: 44px;
height: 44px;
min-height: 44px;
justify-content: center;
gap: 0;
padding: 0;
}
.shell.openclaw-theme.sidebar-collapsed .nav-item.active {
box-shadow:
inset 0 -3px 0 var(--revamp-blue),
var(--revamp-shadow-soft);
box-shadow: 0 8px 18px rgba(37, 99, 235, 0.12);
}
.shell.openclaw-theme.sidebar-collapsed .nav-item-icon {
display: inline-grid;
place-items: center;
}
.shell.openclaw-theme.sidebar-collapsed .nav-item-icon svg {
width: 20px;
height: 20px;
}
.shell.openclaw-theme .sidebar-experts-entry,
.shell.openclaw-theme .sidebar-session-section,
.shell.openclaw-theme .expert-category-item,
.shell.openclaw-theme .sidebar-session-card {
border-color: rgba(59, 130, 246, 0.22);
background: rgba(255, 255, 255, 0.78);
box-shadow: none;
}
.shell.openclaw-theme .sidebar-experts-entry {
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 8px;
overflow: hidden;
padding: 12px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.82);
border-color: rgba(59, 130, 246, 0.32);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.78);
}
.shell.openclaw-theme .sidebar-session-section {
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
overflow: hidden;
padding: 12px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.82);
border-color: rgba(59, 130, 246, 0.32);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.78);
.shell.openclaw-theme.sidebar-collapsed .nav-item-label {
display: none;
}
.shell.openclaw-theme .sidebar-digital-workers-title {
min-height: 28px;
align-items: center;
padding-bottom: 8px;
border-bottom: 1px solid rgba(59, 130, 246, 0.16);
border-bottom: 1px solid rgba(59, 130, 246, 0.08);
letter-spacing: 0;
}
......@@ -371,7 +432,7 @@
min-height: 28px;
align-items: center;
padding-bottom: 8px;
border-bottom: 1px solid rgba(59, 130, 246, 0.16);
border-bottom: 1px solid rgba(59, 130, 246, 0.08);
}
.shell.openclaw-theme .sidebar-section-label {
......@@ -400,8 +461,6 @@
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
scrollbar-color: rgba(94, 164, 216, 0.44) transparent;
}
.shell.openclaw-theme .sidebar-expert-scroll {
......@@ -410,40 +469,17 @@
.shell.openclaw-theme .sidebar-session-list {
display: grid;
gap: 8px;
gap: 10px;
align-content: start;
padding-right: 2px;
}
.shell.openclaw-theme .sidebar-expert-scroll::-webkit-scrollbar,
.shell.openclaw-theme .sidebar-session-list::-webkit-scrollbar {
width: 4px;
}
.shell.openclaw-theme .sidebar-expert-scroll::-webkit-scrollbar-track,
.shell.openclaw-theme .sidebar-session-list::-webkit-scrollbar-track {
background: transparent;
}
.shell.openclaw-theme .sidebar-expert-scroll::-webkit-scrollbar-thumb,
.shell.openclaw-theme .sidebar-session-list::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgba(94, 164, 216, 0.44);
}
.shell.openclaw-theme .sidebar-expert-scroll:hover::-webkit-scrollbar-thumb,
.shell.openclaw-theme .sidebar-expert-scroll:focus-within::-webkit-scrollbar-thumb,
.shell.openclaw-theme .sidebar-session-list:hover::-webkit-scrollbar-thumb,
.shell.openclaw-theme .sidebar-session-list:focus-within::-webkit-scrollbar-thumb {
background: rgba(37, 99, 235, 0.42);
}
.shell.openclaw-theme .expert-category-item {
position: relative;
overflow: hidden;
border-radius: 14px;
border-color: rgba(59, 130, 246, 0.2);
background: #ffffff;
border-radius: 10px;
border-color: transparent;
background: transparent;
box-shadow: none;
transition: border-color 180ms ease, box-shadow 180ms ease, background 180ms ease, color 180ms ease;
}
......@@ -459,8 +495,8 @@
}
.shell.openclaw-theme .expert-category-item:hover {
border-color: rgba(37, 99, 235, 0.44);
box-shadow: var(--revamp-shadow-soft);
border-color: rgba(37, 99, 235, 0.2);
box-shadow: none;
}
.shell.openclaw-theme .expert-category-item:hover::before {
......@@ -470,7 +506,7 @@
.shell.openclaw-theme .expert-category-item.expanded {
border-color: rgba(37, 99, 235, 0.52);
background: #f8fbff;
box-shadow: var(--revamp-shadow-soft);
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.08);
}
.shell.openclaw-theme .expert-category-item.expanded::before {
......@@ -497,7 +533,7 @@
height: 24px;
border-radius: 8px;
background: rgba(37, 99, 235, 0.1);
box-shadow: inset 0 0 0 1px rgba(37, 99, 235, 0.14);
box-shadow: none;
}
.shell.openclaw-theme .expert-category-name {
......@@ -523,23 +559,23 @@
}
.shell.openclaw-theme .expert-category-experts {
gap: 5px;
gap: 8px;
padding: 7px 7px 8px 10px;
}
.shell.openclaw-theme .expert-category-expert-item {
min-height: 34px;
padding: 6px 9px 6px 12px;
border-radius: 12px;
border-color: rgba(59, 130, 246, 0.14);
background: rgba(255, 255, 255, 0.74);
border-radius: 8px;
border-color: transparent;
background: transparent;
color: #31516f;
transition: background 160ms ease, border-color 160ms ease, color 160ms ease, box-shadow 160ms ease;
}
.shell.openclaw-theme .expert-category-expert-item:hover {
background: rgba(255, 255, 255, 0.9);
border-color: rgba(37, 99, 235, 0.34);
background: rgba(255, 255, 255, 0.7);
border-color: transparent;
color: #104eae;
box-shadow: none;
}
......@@ -550,15 +586,27 @@
border-color: var(--revamp-blue-border);
color: var(--revamp-blue-strong);
box-shadow:
inset 4px 0 0 rgba(37, 99, 235, 0.86),
var(--revamp-shadow-soft);
inset 3px 0 0 rgba(37, 99, 235, 0.86),
0 4px 12px rgba(37, 99, 235, 0.06);
}
.shell.openclaw-theme .expert-category-expert-item.active {
background: #ffffff;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(224, 237, 255, 0.92));
box-shadow:
inset 3px 0 0 var(--revamp-cyber-blue),
var(--revamp-shadow-soft);
inset 2px 0 0 var(--revamp-cyber-blue),
0 2px 8px rgba(37, 99, 235, 0.06);
}
.shell.openclaw-theme .expert-category-expert-icon {
color: currentColor;
}
.shell.openclaw-theme .expert-category-expert-name {
color: currentColor;
font-size: 13px;
font-weight: 600;
line-height: 1.45;
letter-spacing: 0;
}
.shell.openclaw-theme .sidebar-session-card {
......@@ -566,29 +614,29 @@
min-height: 50px;
padding: 3px;
border-radius: 14px;
border-color: rgba(59, 130, 246, 0.2);
background: #ffffff;
border-color: transparent;
background: transparent;
box-shadow: none;
}
.shell.openclaw-theme .sidebar-session-card:hover {
border-color: rgba(37, 99, 235, 0.42);
background: #f8fbff;
box-shadow: var(--revamp-shadow-soft);
border-color: rgba(37, 99, 235, 0.2);
background: rgba(255, 255, 255, 0.7);
box-shadow: none;
}
.shell.openclaw-theme .sidebar-session-card.active {
border-color: rgba(37, 99, 235, 0.58);
border-color: rgba(37, 99, 235, 0.42);
background: #ffffff;
box-shadow:
inset 4px 0 0 var(--revamp-blue),
var(--revamp-shadow-soft);
inset 3px 0 0 var(--revamp-blue),
0 4px 12px rgba(37, 99, 235, 0.06);
}
.shell.openclaw-theme .sidebar-session-main {
min-height: 42px;
padding: 0 8px 0 14px;
border-radius: 12px;
border-radius: 8px;
}
.shell.openclaw-theme .sidebar-session-main strong {
......@@ -603,19 +651,16 @@
color: #64748b;
opacity: 0.72;
background: #f1f5f9;
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.26);
}
.shell.openclaw-theme .expert-category-expert-icon {
color: currentColor;
box-shadow: none;
}
.shell.openclaw-theme .expert-category-expert-name {
color: currentColor;
font-size: 13px;
font-weight: 600;
line-height: 1.45;
letter-spacing: 0;
.shell.openclaw-theme .sidebar-session-close svg {
width: 12px;
height: 12px;
display: block;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
}
@keyframes expertPanelReveal {
......
......@@ -30,24 +30,23 @@ test("app shell wires a collapsible sidebar state into the sidebar components",
})
test("sidebar nav omits plugins and keeps the new conversation action visible when expanded", () => {
const navItems = appSidebarSource.match(/\{\s*id:\s*"chat"[\s\S]*?\]\.map/)?.[0] ?? ""
const navItems = appSidebarSource.match(/\{\s*id:\s*"chat"[\s\S]*?\]\.(filter[\s\S]*?\)\.)?map/)?.[0] ?? ""
assert.match(navItems, /id:\s*"chat"/)
assert.match(navItems, /id:\s*"tasks"/)
assert.match(navItems, /id:\s*"knowledge"/)
assert.match(navItems, /id:\s*"settings"/)
assert.doesNotMatch(navItems, /id:\s*"plugins"/)
assert.match(appSidebarSource, /<div className="conversation-sidebar-action">\s*\{!showBindEntry \? sidebarNewSessionAction : null\}\s*<\/div>/)
assert.match(appSidebarSource, /\{!showBindEntry \? <div className="conversation-sidebar-action">\{sidebarNewSessionAction\}<\/div> : null\}/)
})
test("sidebar renders the brand row and hides non-icon content in collapsed mode", () => {
assert.match(sidebarSource, /brandIcon/)
assert.match(sidebarSource, /sidebar-brand/)
assert.match(sidebarSource, /千匠问天/)
assert.match(sidebarSource, /sidebar-collapse-button/)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed\s*\{[\s\S]*?grid-template-columns:\s*64px minmax\(0, 1fr\);/m)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-brand-logo,\s*\n\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-brand-name/)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-brand\s*\{[\s\S]*?grid-template-columns:\s*44px;/m)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-collapse-button\s*\{[\s\S]*?width:\s*44px;[\s\S]*?height:\s*44px;/m)
assert.match(shellStylesSource, /\.shell\.sidebar-collapsed\s*\{[\s\S]*?grid-template-columns:\s*64px minmax\(0, 1fr\);/m)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-header-content,\s*\n\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-brand-logo/)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-brand\s*\{[\s\S]*?width:\s*44px;[\s\S]*?justify-content:\s*center;/m)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-collapse-button\s*\{[\s\S]*?width:\s*38px;[\s\S]*?height:\s*38px;/m)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-top\s*\{[\s\S]*?align-content:\s*start;/m)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.nav-item-label/)
assert.match(themeStylesSource, /\.shell\.openclaw-theme\.sidebar-collapsed \.sidebar-bottom/)
......@@ -61,8 +60,8 @@ test("sidebar keeps brand, navigation, and lower content in fixed grid rows", ()
})
test("new conversation action matches the sidebar navigation control size", () => {
assert.match(cssBlock(themeStylesSource, ".shell.openclaw-theme .nav-item"), /min-height:\s*42px;[\s\S]*?border-radius:\s*10px;/)
assert.match(cssBlock(themeStylesSource, ".shell.openclaw-theme .sidebar-new-session.conversation-new-session"), /width:\s*100%;[\s\S]*?min-height:\s*42px;[\s\S]*?justify-content:\s*center;[\s\S]*?border-radius:\s*10px;/)
assert.match(cssBlock(themeStylesSource, ".shell.openclaw-theme .nav-item"), /min-height:\s*42px;[\s\S]*?border-radius:\s*14px;/)
assert.match(cssBlock(themeStylesSource, ".shell.openclaw-theme .sidebar-new-session.conversation-new-session"), /width:\s*100%;[\s\S]*?min-height:\s*44px;[\s\S]*?justify-content:\s*center;[\s\S]*?border-radius:\s*14px;/)
})
test("knowledge navigation uses a book icon instead of the old document icon", () => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment