feat(frontend): implement SectionAnchorTabs for improved navigation
- Replaced the existing navigation in PromptEditor and PanelAnchorNavigation with the new SectionAnchorTabs component for better code reuse and consistency. - The SectionAnchorTabs component provides a shared interface for section navigation with equal-width columns and an active state indicator.
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
|||||||
ToolPicker,
|
ToolPicker,
|
||||||
} from "@/components/assistant-editor/editor-controls";
|
} from "@/components/assistant-editor/editor-controls";
|
||||||
import type { AssistantForm } from "@/components/assistant-editor/types";
|
import type { AssistantForm } from "@/components/assistant-editor/types";
|
||||||
|
import { SectionAnchorTabs } from "@/components/editor/section-anchor-tabs";
|
||||||
import { SectionCard } from "@/components/editor/section-card";
|
import { SectionCard } from "@/components/editor/section-card";
|
||||||
import { VisionConfigSection } from "@/components/editor/vision-config-section";
|
import { VisionConfigSection } from "@/components/editor/vision-config-section";
|
||||||
import { TurnConfigEditor } from "@/components/turn-config-editor";
|
import { TurnConfigEditor } from "@/components/turn-config-editor";
|
||||||
@@ -315,35 +316,18 @@ export function PromptEditor({
|
|||||||
className="flex h-full min-h-0 overflow-hidden bg-background"
|
className="flex h-full min-h-0 overflow-hidden bg-background"
|
||||||
>
|
>
|
||||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||||
<nav
|
<SectionAnchorTabs
|
||||||
aria-label="提示词配置分区"
|
ariaLabel="提示词配置分区"
|
||||||
className="shrink-0 overflow-x-auto overscroll-none bg-background px-4 pt-3 sm:px-6 sm:pt-4 lg:px-8"
|
sections={promptSections}
|
||||||
>
|
activeSectionId={activeSection}
|
||||||
<div className={debugOpen ? "w-full" : "mx-auto w-full max-w-7xl"}>
|
onSelect={(sectionId) =>
|
||||||
<div className="grid min-w-[28rem] grid-cols-5 border-b border-hairline">
|
scrollToSection(sectionId as PromptSectionId)
|
||||||
{promptSections.map((section) => {
|
}
|
||||||
const active = activeSection === section.id;
|
className="bg-background px-4 pt-3 sm:px-6 sm:pt-4 lg:px-8"
|
||||||
|
contentClassName={
|
||||||
return (
|
debugOpen ? "w-full" : "mx-auto w-full max-w-7xl"
|
||||||
<button
|
}
|
||||||
key={section.id}
|
/>
|
||||||
type="button"
|
|
||||||
aria-current={active ? "location" : undefined}
|
|
||||||
onClick={() => scrollToSection(section.id)}
|
|
||||||
className={[
|
|
||||||
"-mb-px whitespace-nowrap border-b-2 px-2 py-2.5 text-center text-xs transition-colors sm:px-3 sm:text-sm",
|
|
||||||
active
|
|
||||||
? "border-primary text-foreground"
|
|
||||||
: "border-transparent text-muted-foreground hover:bg-surface-strong/50 hover:text-foreground",
|
|
||||||
].join(" ")}
|
|
||||||
>
|
|
||||||
{section.label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared underline section tabs for prompt editor and workflow side panels.
|
||||||
|
* Equal-width columns; active state is a primary bottom border (not pill Tabs).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type SectionAnchorTab = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SectionAnchorTabs({
|
||||||
|
ariaLabel,
|
||||||
|
sections,
|
||||||
|
activeSectionId,
|
||||||
|
onSelect,
|
||||||
|
className,
|
||||||
|
contentClassName,
|
||||||
|
}: {
|
||||||
|
ariaLabel: string;
|
||||||
|
sections: readonly SectionAnchorTab[];
|
||||||
|
activeSectionId: string;
|
||||||
|
onSelect: (sectionId: string) => void;
|
||||||
|
className?: string;
|
||||||
|
/** Optional width/centering wrapper around the tab row (prompt page uses max-w-7xl). */
|
||||||
|
contentClassName?: string;
|
||||||
|
}) {
|
||||||
|
if (sections.length === 0) return null;
|
||||||
|
|
||||||
|
const list = (
|
||||||
|
<div
|
||||||
|
// w-full matters inside overflow-x-auto parents: without it the grid
|
||||||
|
// shrink-wraps to minWidth and tabs look left-clustered.
|
||||||
|
className="grid w-full border-b border-hairline"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${sections.length}, minmax(0, 1fr))`,
|
||||||
|
minWidth: `${Math.max(sections.length * 5.6, 12)}rem`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sections.map((section) => {
|
||||||
|
const active = activeSectionId === section.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={section.id}
|
||||||
|
type="button"
|
||||||
|
aria-current={active ? "location" : undefined}
|
||||||
|
onClick={() => onSelect(section.id)}
|
||||||
|
className={cn(
|
||||||
|
"-mb-px whitespace-nowrap border-b-2 px-2 py-2.5 text-center text-xs transition-colors sm:px-3 sm:text-sm",
|
||||||
|
active
|
||||||
|
? "border-primary text-foreground"
|
||||||
|
: "border-transparent text-muted-foreground hover:bg-surface-strong/50 hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{section.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className={cn("shrink-0 overflow-x-auto overscroll-none", className)}
|
||||||
|
>
|
||||||
|
{contentClassName ? (
|
||||||
|
<div className={contentClassName}>{list}</div>
|
||||||
|
) : (
|
||||||
|
list
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
import { SectionAnchorTabs } from "@/components/editor/section-anchor-tabs";
|
||||||
|
|
||||||
export type PanelAnchorSection = {
|
export type PanelAnchorSection = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -128,33 +130,13 @@ export function PanelAnchorNavigation({
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-0 flex-1 flex-col bg-canvas-soft">
|
<div className="flex min-h-0 flex-1 flex-col bg-canvas-soft">
|
||||||
{sections.length > 1 && (
|
{sections.length > 1 && (
|
||||||
<nav
|
<SectionAnchorTabs
|
||||||
aria-label={ariaLabel}
|
ariaLabel={ariaLabel}
|
||||||
className="shrink-0 overflow-x-auto overscroll-none border-b border-hairline bg-card px-4"
|
sections={sections}
|
||||||
>
|
activeSectionId={resolvedActiveSection}
|
||||||
<div className="flex min-w-max">
|
onSelect={scrollToSection}
|
||||||
{sections.map((section) => {
|
className="bg-card px-4"
|
||||||
const active = resolvedActiveSection === section.id;
|
/>
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={section.id}
|
|
||||||
type="button"
|
|
||||||
aria-current={active ? "location" : undefined}
|
|
||||||
onClick={() => scrollToSection(section.id)}
|
|
||||||
className={[
|
|
||||||
"-mb-px min-w-24 whitespace-nowrap border-b-2 px-3 py-2.5 text-center text-xs transition-colors",
|
|
||||||
active
|
|
||||||
? "border-primary text-foreground"
|
|
||||||
: "border-transparent text-muted-foreground hover:bg-surface-strong/50 hover:text-foreground",
|
|
||||||
].join(" ")}
|
|
||||||
>
|
|
||||||
{section.label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user