mirror of
https://github.com/dataelement/bisheng.git
synced 2026-08-29 01:22:31 +08:00
feat(knowledge): folder-path breadcrumb above the space header
Inside a folder the header only showed the current folder's name — no sense of depth, and getting back up meant repeated back-clicks. A breadcrumb row (design 2075:8134) now sits above the header on desktop: space name, then the ancestor chain, leaf last. - Parent segments are clickable, capped at 8 CJK chars (96px @12px) with a full-name tooltip only when actually truncated. - Chains deeper than 5 levels collapse the middle into an ellipsis whose hover card lists the hidden folders. - Hidden at the space root so the single-level page keeps its layout; when shown, the leaf title drops to font-normal — the breadcrumb carries the hierarchy, the title no longer has to. Suppressions: pruned entries the header edit and the latest merge made stale (shrink-only). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -741,11 +741,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/Chat/Messages/DeepThinkingGroup.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"src/components/Chat/Messages/HoverButtons.tsx": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 2
|
||||
@@ -808,9 +803,6 @@
|
||||
}
|
||||
},
|
||||
"src/components/Chat/Messages/ThinkingContent.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
@@ -3345,7 +3337,7 @@
|
||||
},
|
||||
"src/pages/knowledge/SpaceDetail/KnowledgeSpaceHeader.tsx": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 5
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"src/pages/knowledge/SpaceDetail/KnowledgeSpaceShareDialog.tsx": {
|
||||
@@ -3436,7 +3428,7 @@
|
||||
},
|
||||
"src/pages/knowledge/index.tsx": {
|
||||
"@typescript-eslint/no-unused-vars": {
|
||||
"count": 3
|
||||
"count": 2
|
||||
},
|
||||
"no-console": {
|
||||
"count": 3
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useState } from "react";
|
||||
import { Outlined } from "bisheng-icons";
|
||||
import { cn } from "~/utils";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/Tooltip2";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "~/components/ui/HoverCard";
|
||||
|
||||
interface BreadcrumbNode {
|
||||
id?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface KnowledgeBreadcrumbProps {
|
||||
spaceName: string;
|
||||
/** Ancestor chain from root folder to the current folder (leaf last); space root NOT included. */
|
||||
currentPath: BreadcrumbNode[];
|
||||
onNavigateFolder: (folderId?: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** Parent segment width cap: 8 CJK characters at 12px (design "最大长度为8个中文字符"). */
|
||||
const PARENT_MAX_WIDTH = "max-w-[96px]";
|
||||
/** When the chain exceeds 5 levels, collapse the middle into an ellipsis (design 2075:8134). */
|
||||
const COLLAPSE_THRESHOLD = 5;
|
||||
|
||||
function Separator() {
|
||||
return <Outlined.Right className="size-4 shrink-0 text-text-4" aria-hidden />;
|
||||
}
|
||||
|
||||
/** Clickable parent segment: hover highlights with the brand color; long names truncate with a tooltip. */
|
||||
function ParentCrumb({ node, onNavigate }: { node: BreadcrumbNode; onNavigate: (folderId?: string) => void }) {
|
||||
// The full-name tooltip only shows when the name actually overflows the width cap.
|
||||
const [overflowing, setOverflowing] = useState(false);
|
||||
|
||||
const button = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onNavigate(node.id)}
|
||||
onMouseEnter={(e) => setOverflowing(e.currentTarget.scrollWidth > e.currentTarget.clientWidth)}
|
||||
className={cn(
|
||||
"shrink-0 truncate text-text-3 transition-colors hover:text-blue-600",
|
||||
PARENT_MAX_WIDTH,
|
||||
)}
|
||||
>
|
||||
{node.name}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
{overflowing && (
|
||||
<TooltipContent noArrow side="bottom" className="z-[999] bg-white px-3 py-2 text-sm text-[#4e5969] shadow-md">
|
||||
{node.name}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Folder path breadcrumb above the knowledge-space header title (desktop only; design 2075:8134).
|
||||
* Hidden at the space root ("single level" case) — the caller only renders it inside a folder.
|
||||
*/
|
||||
export function KnowledgeBreadcrumb({ spaceName, currentPath, onNavigateFolder, className }: KnowledgeBreadcrumbProps) {
|
||||
// Full chain: space root first, current folder last.
|
||||
const items: BreadcrumbNode[] = [{ id: undefined, name: spaceName }, ...currentPath];
|
||||
const current = items[items.length - 1];
|
||||
const collapse = items.length > COLLAPSE_THRESHOLD;
|
||||
// Collapsed layout keeps the first item and the last two; the rest hide behind an ellipsis.
|
||||
const headParents = collapse ? [items[0]] : items.slice(0, -1);
|
||||
const hiddenParents = collapse ? items.slice(1, -2) : [];
|
||||
const tailParents = collapse ? [items[items.length - 2]] : [];
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label="breadcrumb"
|
||||
className={cn("flex min-w-0 items-center gap-0.5 text-caption leading-6 text-text-3", className)}
|
||||
>
|
||||
{headParents.map((node) => (
|
||||
<span key={node.id ?? "space-root"} className="flex shrink-0 items-center gap-0.5">
|
||||
<ParentCrumb node={node} onNavigate={onNavigateFolder} />
|
||||
<Separator />
|
||||
</span>
|
||||
))}
|
||||
{collapse && (
|
||||
<span className="flex shrink-0 items-center gap-0.5">
|
||||
<HoverCard openDelay={100} closeDelay={150}>
|
||||
<HoverCardTrigger asChild>
|
||||
<span className="cursor-pointer px-0.5 text-text-3 transition-colors hover:text-blue-600">
|
||||
...
|
||||
</span>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
align="start"
|
||||
className="flex w-auto max-w-[420px] flex-wrap items-center gap-x-0.5 gap-y-1 rounded-lg border border-border-base bg-white px-4 py-2 text-caption leading-6 text-text-3 shadow-md"
|
||||
>
|
||||
{hiddenParents.map((node, idx) => (
|
||||
<span key={node.id} className="flex shrink-0 items-center gap-0.5">
|
||||
{idx > 0 && <Separator />}
|
||||
<ParentCrumb node={node} onNavigate={onNavigateFolder} />
|
||||
</span>
|
||||
))}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
<Separator />
|
||||
</span>
|
||||
)}
|
||||
{tailParents.map((node) => (
|
||||
<span key={node.id ?? "space-root"} className="flex shrink-0 items-center gap-0.5">
|
||||
<ParentCrumb node={node} onNavigate={onNavigateFolder} />
|
||||
<Separator />
|
||||
</span>
|
||||
))}
|
||||
{/* Current page: not clickable, no hover effect, only limited by the row width. */}
|
||||
<span aria-current="page" className="min-w-0 truncate whitespace-nowrap">
|
||||
{current.name}
|
||||
</span>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/Tooltip2";
|
||||
import { CopyShareLinkButton } from "~/components/CopyShareLinkButton";
|
||||
import { KnowledgeBreadcrumb } from "./KnowledgeBreadcrumb";
|
||||
import { useLocalize, useMediaQuery, usePrefersMobileLayout } from "~/hooks";
|
||||
import { knowledgeUploadCapabilities } from "../knowledgeUploadCapabilities";
|
||||
|
||||
@@ -383,7 +384,21 @@ export function KnowledgeSpaceHeader({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-8 items-center justify-between gap-3 pt-4 pb-4 max-[767px]:gap-2 max-[767px]:pb-3">
|
||||
<div className="flex flex-col">
|
||||
{/* Folder-path breadcrumb (desktop only — this header never renders on mobile).
|
||||
Hidden at the space root: with a single level the page content shifts up (design 2075:8134). */}
|
||||
{currentPath.length > 0 && (
|
||||
<KnowledgeBreadcrumb
|
||||
className="pt-3"
|
||||
spaceName={space.name}
|
||||
currentPath={currentPath}
|
||||
onNavigateFolder={onNavigateFolder}
|
||||
/>
|
||||
)}
|
||||
<div className={cn(
|
||||
"flex min-h-8 items-center justify-between gap-3 pb-4 max-[767px]:gap-2 max-[767px]:pb-3",
|
||||
currentPath.length > 0 ? "pt-1" : "pt-4",
|
||||
)}>
|
||||
|
||||
{/* 左侧:根目录显示空间标题 + 信息 + 分享;进入文件夹后显示返回按钮 + 分隔线 + 当前文件夹名(设计稿 11772:70584) */}
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1 text-sm">
|
||||
@@ -403,7 +418,7 @@ export function KnowledgeSpaceHeader({
|
||||
</button>
|
||||
<div className="mx-1 h-4 w-px shrink-0 bg-[#e5e6eb]" aria-hidden />
|
||||
*/}
|
||||
<h1 className="min-w-0 truncate text-base font-medium text-[#1d2129] max-[767px]:text-[16px] max-[767px]:leading-6">
|
||||
<h1 className="min-w-0 truncate text-base font-normal text-[#1d2129] max-[767px]:text-[16px] max-[767px]:leading-6">
|
||||
{currentPath[currentPath.length - 1]?.name || space.name}
|
||||
</h1>
|
||||
</>
|
||||
@@ -460,5 +475,6 @@ export function KnowledgeSpaceHeader({
|
||||
{batchAndAddActions}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user