refactor(approval): the filter tabs come from the library, the title moves into the list column

The pane drew its own filter row — bare buttons with a hand-rolled 2px
bottom border and a hardcoded #212121 for the selected one. That is the
library's Tabs in `variant="neutral"`, so it now renders one, with
`divider={false}` because the list column already draws its own edge.

The section title moves out of the page header and into the list column
via a new `listHeader` prop. It used to sit above the whole pane as a
full-width strip, which pushed the detail column down and left it running
short of its container; with the title inside the list column, the detail
column runs the full height. Both titles are put on the same line box
(text-base / leading-8 at pt-4) so they read as one row across the two
columns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kinyoo
2026-08-28 14:20:27 +08:00
parent fd1cf7921b
commit 71b58c62cd
3 changed files with 57 additions and 41 deletions
@@ -31,12 +31,12 @@ export function DetailHeader({ title, status, instanceStatus, scope, serialNo, s
}) {
return (
// Pinned to the top of the scrolling detail pane so the title/status/serial stay visible while the body scrolls.
<div className="sticky top-0 z-10 -mx-5 mb-5 border-b border-fill-2 bg-white px-5 pb-3 pt-3">
<div className="sticky top-0 z-10 -mx-5 mb-5 border-b border-fill-2 bg-white px-5 pb-3 pt-4">
<div className="flex items-start gap-3">
{/* Compact-only back control — sits to the left of the detail title, split by a short vertical divider.
h-6 matches the title line so the arrow centers against it under items-start. */}
h-8 matches the title line so the arrow centers against it under items-start. */}
{onBack && (
<div className="flex h-6 shrink-0 items-center gap-3 md:hidden">
<div className="flex h-8 shrink-0 items-center gap-3 md:hidden">
<button type="button" onClick={onBack} aria-label={localize("com_approval_back")} className="flex items-center text-text-3">
<Outlined.ArrowLeft className="h-4 w-4" />
</button>
@@ -46,7 +46,9 @@ export function DetailHeader({ title, status, instanceStatus, scope, serialNo, s
{/* Title + serial share one column so the serial line aligns with the title, not the back arrow. */}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-3">
<h3 className="min-w-0 flex-1 text-[16px] font-semibold text-text-primary leading-snug">{title || "--"}</h3>
{/* Same line box as the list column's section heading (text-base/leading-8 at pt-4),
so both titles sit on one line across the two columns. */}
<h3 className="min-w-0 flex-1 text-base font-semibold leading-8 text-text-primary">{title || "--"}</h3>
<StatusBadge status={status} instanceStatus={instanceStatus} scope={scope} localize={localize} />
</div>
<p className="mt-1.5 text-[13px] text-text-3">
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { Tabs } from "@bisheng/ui";
import { useEffect, useMemo, useState, type ReactNode } from "react";
import {
decideApprovalTaskApi,
getApprovalInstanceDetailApi,
@@ -40,6 +41,12 @@ export interface ApprovalPaneProps {
setCompactView: (view: "list" | "detail") => void;
/** Fired after any action that can change the pending-task count. */
onPendingCountMaybeChanged?: () => void;
/**
* Rendered at the top of the LIST column, not above the whole pane — the detail
* column then runs the full height of its container instead of starting below a
* full-width header strip.
*/
listHeader?: ReactNode;
}
export function ApprovalPane({
@@ -49,6 +56,7 @@ export function ApprovalPane({
compactView,
setCompactView,
onPendingCountMaybeChanged,
listHeader,
}: ApprovalPaneProps) {
const localize = useLocalize();
const { showToast } = useToastContext();
@@ -292,26 +300,32 @@ export function ApprovalPane({
<>
{/* Left list — hidden in compact detail view */}
<div className={cn("flex min-h-0 flex-col border-r border-fill-2 bg-white", compactView === "detail" && "hidden md:flex")}>
<div className="flex gap-2 px-3 pt-3 pb-2">
{activeTab === "my_tasks"
? (["pending_me", "processed"] as TaskFilter[]).map((f) => (
<button key={f} type="button"
className={cn(
"h-auto whitespace-nowrap rounded-none border-0 border-b-2 border-transparent bg-transparent px-2 py-[5px] text-sm leading-none transition-colors fine-pointer:hover:text-text-1",
taskFilter === f ? "border-[#212121] text-text-1 font-medium" : "text-text-3 font-normal")}
onClick={() => setTaskFilter(f)}>
{f === "pending_me" ? localize("com_approval_task_filter_pending") : localize("com_approval_task_filter_processed")}
</button>
))
: (["in_progress", "completed"] as RequestsFilter[]).map((f) => (
<button key={f} type="button"
className={cn(
"h-auto whitespace-nowrap rounded-none border-0 border-b-2 border-transparent bg-transparent px-2 py-[5px] text-sm leading-none transition-colors fine-pointer:hover:text-text-1",
requestsFilter === f ? "border-[#212121] text-text-1 font-medium" : "text-text-3 font-normal")}
onClick={() => setRequestsFilter(f)}>
{f === "in_progress" ? localize("com_approval_status_pending") : localize("com_approval_tab_completed")}
</button>
))}
{listHeader}
{/* 待我处理/已处理 · 审批中/已完成 — design-system Tabs (line type, ink
variant: the surrounding column already draws its own edges, so no
divider, and an accent here would fight the status badges in the list). */}
<div className="px-3 pt-3 pb-2">
<Tabs
size="medium"
variant="neutral"
divider={false}
items={
activeTab === "my_tasks"
? [
{ key: "pending_me", label: localize("com_approval_task_filter_pending") },
{ key: "processed", label: localize("com_approval_task_filter_processed") },
]
: [
{ key: "in_progress", label: localize("com_approval_status_pending") },
{ key: "completed", label: localize("com_approval_tab_completed") },
]
}
activeKey={activeTab === "my_tasks" ? taskFilter : requestsFilter}
onChange={(key) => {
if (activeTab === "my_tasks") setTaskFilter(key as TaskFilter);
else setRequestsFilter(key as RequestsFilter);
}}
/>
</div>
{/* Search box — reuse the app-center search field style (always expanded + clear button) */}
@@ -203,22 +203,22 @@ export default function SettingsPage() {
const paneTitleClass = "text-base font-semibold leading-8 text-text-1";
const content = isApproval ? (
<div className="flex min-h-0 flex-1 flex-col">
<h2 className={cn("hidden shrink-0 px-5 pt-4 md:block", paneTitleClass)}>
{sectionTitle}
</h2>
<div
className="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[300px_minmax(0,1fr)]"
>
<ApprovalPane
open
activeTab={approvalTab}
target={deepLink ?? undefined}
compactView={compactView}
setCompactView={setCompactView}
onPendingCountMaybeChanged={refreshCount}
/>
</div>
// The title rides inside the list column (via listHeader), so the detail column
// runs the full height of the content panel instead of starting below a header strip.
<div className="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[300px_minmax(0,1fr)]">
<ApprovalPane
open
activeTab={approvalTab}
target={deepLink ?? undefined}
compactView={compactView}
setCompactView={setCompactView}
onPendingCountMaybeChanged={refreshCount}
listHeader={
<h2 className={cn("hidden shrink-0 px-3 pt-4 md:block", paneTitleClass)}>
{sectionTitle}
</h2>
}
/>
</div>
) : isNotifications ? (
// Same single-block shell as 账号信息 / 通用: one padded pane with a centered