refactor(diagram): share toolbar controls
This commit is contained in:
1 parent
2bac88b110
commit
cf204dc411
5 files changed
+196
-87
No files matched your search
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const source = readFileSync(new URL("./DiagramEditorPane.tsx", import.meta.url), "utf8");
|
||||
const toolbarSource = readFileSync(new URL("./DiagramToolbar.tsx", import.meta.url), "utf8");
|
||||
const globalStyles = readFileSync(new URL("../styles/globals.css", import.meta.url), "utf8");
|
||||
|
||||
describe("diagram editor keyboard workflow", () => {
|
||||
@@ -84,9 +85,26 @@ describe("diagram editor canvas surface", () => {
|
||||
});
|
||||
|
||||
test("labels auto layout directly instead of relying on an ambiguous icon", () => {
|
||||
expect(source).toContain('<WandSparkles className="h-4 w-4" />');
|
||||
expect(source).toContain('<span>{t("diagram.autoLayout")}</span>');
|
||||
expect(source).not.toContain('<Button size="icon" variant="ghost" aria-label={t("diagram.autoLayout")}');
|
||||
expect(toolbarSource).toContain('<WandSparkles className="h-4 w-4" />');
|
||||
expect(toolbarSource).toContain('<span>{t("diagram.autoLayout")}</span>');
|
||||
expect(toolbarSource).not.toContain('<Button size="icon" variant="ghost" aria-label={t("diagram.autoLayout")}');
|
||||
});
|
||||
|
||||
test("delegates every diagram kind to one shared toolbar shell", () => {
|
||||
expect(source).toContain("<DiagramToolbar");
|
||||
expect(source).toContain("leading={!readOnly ? (");
|
||||
expect(toolbarSource).toContain('role="toolbar"');
|
||||
expect(toolbarSource).toContain("{leading ? <>{leading}<ToolbarDivider /></> : null}");
|
||||
expect(toolbarSource).toContain("{selectionEditor}");
|
||||
});
|
||||
|
||||
test("reuses the architecture shape-library trigger across every diagram kind", () => {
|
||||
expect(toolbarSource).toContain("export const DiagramToolbarAddTrigger");
|
||||
expect(toolbarSource).toContain("<DropdownMenuTrigger asChild>");
|
||||
expect(toolbarSource).toContain('<Button size="sm" variant="soft" onPointerEnter={onPointerEnter}>');
|
||||
expect(toolbarSource).toContain('<Boxes className="h-4 w-4" />');
|
||||
expect(toolbarSource).toContain('{t("diagram.componentLibrary")}');
|
||||
expect(source.match(/<DiagramToolbarAddTrigger onPointerEnter=/g)).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("exposes connection handles on flowcharts and architecture components with safe connection rules", () => {
|
||||
@@ -129,7 +147,6 @@ describe("diagram editor canvas surface", () => {
|
||||
|
||||
test("organizes architecture resources in a searchable category library", () => {
|
||||
expect(source).toContain("ARCHITECTURE_LIBRARY_CATEGORIES");
|
||||
expect(source).toContain('t("diagram.componentLibrary")');
|
||||
expect(source).toContain('onPointerEnter={() => setOpen(true)}');
|
||||
expect(source).toContain('t("diagram.componentSearch")');
|
||||
expect(source).toContain('labelKey: "diagram.componentCategoryExperience"');
|
||||
@@ -160,9 +177,9 @@ describe("diagram editor canvas surface", () => {
|
||||
test("opens every diagram insertion library immediately on pointer hover", () => {
|
||||
expect(source).toContain("const DiagramInsertMenu = ({");
|
||||
expect(source.match(/onPointerEnter=\{\(\) => setOpen\(true\)\}/g)?.length).toBe(2);
|
||||
expect(source).toContain('label={t("diagram.addTopic")}');
|
||||
expect(source).toContain('label: t("diagram.addTopic")');
|
||||
expect(source).toContain('label: t("diagram.addSiblingTopic")');
|
||||
expect(source).toContain('label={t("diagram.addStep")}');
|
||||
expect(source).toContain('label: t("diagram.addStep")');
|
||||
expect(source).toContain('label: t("diagram.addDecision")');
|
||||
expect(source).toContain('label: t("diagram.addTerminator")');
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
AppWindow,
|
||||
Blocks,
|
||||
Box,
|
||||
Boxes,
|
||||
BrickWall,
|
||||
Cable,
|
||||
Check,
|
||||
@@ -26,7 +25,6 @@ import {
|
||||
Diamond,
|
||||
Database,
|
||||
DatabaseZap,
|
||||
Download,
|
||||
EthernetPort,
|
||||
FileCode2,
|
||||
FileClock,
|
||||
@@ -51,7 +49,6 @@ import {
|
||||
Network,
|
||||
Pencil,
|
||||
RadioTower,
|
||||
Redo2,
|
||||
RefreshCw,
|
||||
RotateCcw,
|
||||
Router,
|
||||
@@ -62,13 +59,9 @@ import {
|
||||
Smartphone,
|
||||
SquareFunction,
|
||||
Trash2,
|
||||
Undo2,
|
||||
WandSparkles,
|
||||
Webhook,
|
||||
Workflow,
|
||||
Zap,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -93,9 +86,9 @@ import { AppConfirmDialog } from "@/components/dialogs/ConfirmDialogs";
|
||||
import { RevisionHistoryDialog } from "@/components/dialogs/RevisionHistoryDialog";
|
||||
import { ShareMemoDialog } from "@/components/dialogs/ShareMemoDialog";
|
||||
import { ClipboardCopyNotice } from "@/components/ClipboardCopyNotice";
|
||||
import { DiagramToolbar, DiagramToolbarAddTrigger } from "@/components/DiagramToolbar";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||
import { useAppearanceTheme } from "@/components/ThemeProvider";
|
||||
@@ -331,11 +324,7 @@ const ArchitectureComponentLibrary = ({
|
||||
setOpen(nextOpen);
|
||||
if (!nextOpen) setQuery("");
|
||||
}}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="outline" onPointerEnter={() => setOpen(true)}>
|
||||
<Boxes className="h-4 w-4" />{t("diagram.componentLibrary")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DiagramToolbarAddTrigger onPointerEnter={() => setOpen(true)} />
|
||||
<DropdownMenuContent align="start" className="max-h-[min(36rem,calc(100vh-8rem))] w-[min(30rem,calc(100vw-2rem))] overflow-y-auto p-0">
|
||||
<div className="sticky top-0 z-10 border-b border-slate-200 bg-white p-2.5">
|
||||
<div className="relative">
|
||||
@@ -398,24 +387,14 @@ const ArchitectureComponentLibrary = ({
|
||||
};
|
||||
|
||||
const DiagramInsertMenu = ({
|
||||
icon: TriggerIcon,
|
||||
items,
|
||||
label,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
items: Array<{ icon: LucideIcon; label: string; onSelect: () => void }>;
|
||||
label: string;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="soft" onPointerEnter={() => setOpen(true)}>
|
||||
<TriggerIcon className="h-4 w-4" />
|
||||
{label}
|
||||
<ChevronDown className="h-3.5 w-3.5 text-slate-400" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DiagramToolbarAddTrigger onPointerEnter={() => setOpen(true)} />
|
||||
<DropdownMenuContent align="start">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
@@ -2150,12 +2129,14 @@ export const DiagramEditorPane = ({
|
||||
</header>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-1 border-b border-slate-200 bg-white px-3 py-2">
|
||||
{!readOnly && (
|
||||
<DiagramToolbar
|
||||
appearance={resolvedTheme}
|
||||
canRedo={historyState.redo}
|
||||
canUndo={historyState.undo}
|
||||
hasSelection={hasSelection}
|
||||
leading={!readOnly ? (
|
||||
document.kind === "mind-map" ? (
|
||||
<DiagramInsertMenu
|
||||
icon={GitBranch}
|
||||
label={t("diagram.addTopic")}
|
||||
items={[
|
||||
{ icon: GitBranch, label: t("diagram.addTopic"), onSelect: () => addNode("topic", { relation: "child" }) },
|
||||
{ icon: ListTree, label: t("diagram.addSiblingTopic"), onSelect: () => addNode("topic", { relation: "sibling" }) },
|
||||
@@ -2171,8 +2152,6 @@ export const DiagramEditorPane = ({
|
||||
/>
|
||||
) : (
|
||||
<DiagramInsertMenu
|
||||
icon={Box}
|
||||
label={t("diagram.addStep")}
|
||||
items={[
|
||||
{ icon: Box, label: t("diagram.addStep"), onSelect: () => addNode("process") },
|
||||
{ icon: Diamond, label: t("diagram.addDecision"), onSelect: () => addNode("decision") },
|
||||
@@ -2180,58 +2159,38 @@ export const DiagramEditorPane = ({
|
||||
]}
|
||||
/>
|
||||
)
|
||||
) : undefined}
|
||||
onAutoLayout={applyAutoLayout}
|
||||
onDeleteSelection={removeSelected}
|
||||
onExport={exportDiagram}
|
||||
onFit={() => {
|
||||
const graph = graphRef.current;
|
||||
if (graph) fitDiagramContent(graph, document, containerRef.current);
|
||||
}}
|
||||
onRedo={() => runHistoryAction("redo")}
|
||||
onThemeChange={applyTheme}
|
||||
onUndo={() => runHistoryAction("undo")}
|
||||
onZoomIn={() => graphRef.current?.zoom(0.1)}
|
||||
onZoomOut={() => graphRef.current?.zoom(-0.1)}
|
||||
readOnly={readOnly}
|
||||
selectionEditor={(
|
||||
<>
|
||||
{selectedNodeId && !readOnly && (
|
||||
<div className="ml-auto flex min-w-[220px] flex-1 items-center gap-2 sm:max-w-sm">
|
||||
<span className="shrink-0 text-xs font-medium text-slate-500">{t("diagram.nodeText")}</span>
|
||||
<Input value={selectedNodeLabel} maxLength={120} onChange={(event) => updateSelectedLabel(event.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
{selectedEdgeId && !readOnly && (
|
||||
<div className="ml-auto flex min-w-[220px] flex-1 items-center gap-2 sm:max-w-sm">
|
||||
<span className="shrink-0 text-xs font-medium text-slate-500">{t("diagram.edgeText")}</span>
|
||||
<Input value={selectedEdgeLabel} maxLength={80} onChange={(event) => updateSelectedEdgeLabel(event.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<span className="mx-1 h-5 w-px bg-slate-200" />
|
||||
<Tooltip><TooltipTrigger asChild><Button size="icon" variant="ghost" aria-label={t("diagram.undo")} disabled={!historyState.undo || readOnly} onClick={() => runHistoryAction("undo")}><Undo2 className="h-4 w-4" /></Button></TooltipTrigger><TooltipContent>{t("diagram.undo")}</TooltipContent></Tooltip>
|
||||
<Tooltip><TooltipTrigger asChild><Button size="icon" variant="ghost" aria-label={t("diagram.redo")} disabled={!historyState.redo || readOnly} onClick={() => runHistoryAction("redo")}><Redo2 className="h-4 w-4" /></Button></TooltipTrigger><TooltipContent>{t("diagram.redo")}</TooltipContent></Tooltip>
|
||||
{!readOnly && <Tooltip><TooltipTrigger asChild><Button size="icon" variant="ghost" aria-label={t("diagram.deleteSelection")} disabled={!hasSelection} onClick={removeSelected}><Trash2 className="h-4 w-4" /></Button></TooltipTrigger><TooltipContent>{t("diagram.deleteSelection")}</TooltipContent></Tooltip>}
|
||||
{!readOnly && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="sm" variant="ghost" className="gap-1.5 px-2.5" aria-label={t("diagram.autoLayout")} onClick={applyAutoLayout}>
|
||||
<WandSparkles className="h-4 w-4" />
|
||||
<span>{t("diagram.autoLayout")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("diagram.autoLayout")}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<span className="mx-1 h-5 w-px bg-slate-200" />
|
||||
<Tooltip><TooltipTrigger asChild><Button size="icon" variant="ghost" aria-label={t("diagram.zoomOut")} onClick={() => graphRef.current?.zoom(-0.1)}><ZoomOut className="h-4 w-4" /></Button></TooltipTrigger><TooltipContent>{t("diagram.zoomOut")}</TooltipContent></Tooltip>
|
||||
<Tooltip><TooltipTrigger asChild><Button size="icon" variant="ghost" aria-label={t("diagram.zoomIn")} onClick={() => graphRef.current?.zoom(0.1)}><ZoomIn className="h-4 w-4" /></Button></TooltipTrigger><TooltipContent>{t("diagram.zoomIn")}</TooltipContent></Tooltip>
|
||||
<Tooltip><TooltipTrigger asChild><Button size="icon" variant="ghost" aria-label={t("diagram.fit")} onClick={() => { const graph = graphRef.current; if (graph) fitDiagramContent(graph, document, containerRef.current); }}><Maximize2 className="h-4 w-4" /></Button></TooltipTrigger><TooltipContent>{t("diagram.fit")}</TooltipContent></Tooltip>
|
||||
<Select value={theme} disabled={readOnly} onValueChange={(value) => applyTheme(value as DiagramTheme)}>
|
||||
<SelectTrigger className="h-8 w-[8.5rem] gap-2" aria-label={t("diagram.theme")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="brand" textValue={t("diagram.themeBrand")}><span className="flex items-center gap-2"><span className="h-3 w-3 rounded-full border border-black/10" style={{ background: resolveDiagramPalette("brand", resolvedTheme).topicFill }} />{t("diagram.themeBrand")}</span></SelectItem>
|
||||
<SelectItem value="ocean" textValue={t("diagram.themeOcean")}><span className="flex items-center gap-2"><span className="h-3 w-3 rounded-full border border-black/10" style={{ background: resolveDiagramPalette("ocean", resolvedTheme).topicFill }} />{t("diagram.themeOcean")}</span></SelectItem>
|
||||
<SelectItem value="ink" textValue={t("diagram.themeInk")}><span className="flex items-center gap-2"><span className="h-3 w-3 rounded-full border border-black/10" style={{ background: resolveDiagramPalette("ink", resolvedTheme).nodeFill }} />{t("diagram.themeInk")}</span></SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="outline"><Download className="h-4 w-4" />{t("diagram.export")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onSelect={() => exportDiagram("png")}><FileImage className="h-4 w-4" />{t("diagram.exportPng")}</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => exportDiagram("svg")}><FileCode2 className="h-4 w-4" />{t("diagram.exportSvg")}</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{selectedNodeId && !readOnly && (
|
||||
<div className="ml-auto flex min-w-[220px] flex-1 items-center gap-2 sm:max-w-sm">
|
||||
<span className="shrink-0 text-xs font-medium text-slate-500">{t("diagram.nodeText")}</span>
|
||||
<Input value={selectedNodeLabel} maxLength={120} onChange={(event) => updateSelectedLabel(event.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
{selectedEdgeId && !readOnly && (
|
||||
<div className="ml-auto flex min-w-[220px] flex-1 items-center gap-2 sm:max-w-sm">
|
||||
<span className="shrink-0 text-xs font-medium text-slate-500">{t("diagram.edgeText")}</span>
|
||||
<Input value={selectedEdgeLabel} maxLength={80} onChange={(event) => updateSelectedEdgeLabel(event.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
theme={theme}
|
||||
/>
|
||||
<div className="relative min-h-0 flex-1">
|
||||
<div ref={containerRef} className="edgeever-diagram-canvas absolute inset-0 touch-none outline-none" data-diagram-appearance={resolvedTheme} data-diagram-kind={document.kind} data-diagram-theme={theme} tabIndex={0} aria-label={t("diagram.canvas", { type: kindLabel })} />
|
||||
{flowQuickCreate ? (
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import type { PointerEventHandler, ReactNode } from "react";
|
||||
import {
|
||||
Boxes,
|
||||
ChevronDown,
|
||||
Download,
|
||||
FileCode2,
|
||||
FileImage,
|
||||
Maximize2,
|
||||
Redo2,
|
||||
Trash2,
|
||||
Undo2,
|
||||
WandSparkles,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { DiagramTheme } from "@edgeever/shared";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { resolveDiagramPalette, type DiagramAppearance } from "@/lib/diagram-theme";
|
||||
|
||||
type DiagramToolbarProps = {
|
||||
appearance: DiagramAppearance;
|
||||
canRedo: boolean;
|
||||
canUndo: boolean;
|
||||
hasSelection: boolean;
|
||||
leading?: ReactNode;
|
||||
onAutoLayout: () => void;
|
||||
onDeleteSelection: () => void;
|
||||
onExport: (format: "png" | "svg") => void;
|
||||
onFit: () => void;
|
||||
onRedo: () => void;
|
||||
onThemeChange: (theme: DiagramTheme) => void;
|
||||
onUndo: () => void;
|
||||
onZoomIn: () => void;
|
||||
onZoomOut: () => void;
|
||||
readOnly: boolean;
|
||||
selectionEditor?: ReactNode;
|
||||
theme: DiagramTheme;
|
||||
};
|
||||
|
||||
const ToolbarDivider = () => <span className="mx-1 h-5 w-px shrink-0 bg-slate-200" />;
|
||||
|
||||
export const DiagramToolbarAddTrigger = ({
|
||||
onPointerEnter,
|
||||
}: {
|
||||
onPointerEnter: PointerEventHandler<HTMLButtonElement>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="soft" onPointerEnter={onPointerEnter}>
|
||||
<Boxes className="h-4 w-4" />
|
||||
{t("diagram.componentLibrary")}
|
||||
<ChevronDown className="h-3.5 w-3.5 text-slate-400" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
export const DiagramToolbar = ({
|
||||
appearance,
|
||||
canRedo,
|
||||
canUndo,
|
||||
hasSelection,
|
||||
leading,
|
||||
onAutoLayout,
|
||||
onDeleteSelection,
|
||||
onExport,
|
||||
onFit,
|
||||
onRedo,
|
||||
onThemeChange,
|
||||
onUndo,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
readOnly,
|
||||
selectionEditor,
|
||||
theme,
|
||||
}: DiagramToolbarProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-1 border-b border-slate-200 bg-white px-3 py-2" role="toolbar" aria-label={t("diagram.toolbar")}>
|
||||
{leading ? <>{leading}<ToolbarDivider /></> : null}
|
||||
<Tooltip><TooltipTrigger asChild><Button size="icon" variant="ghost" aria-label={t("diagram.undo")} disabled={!canUndo || readOnly} onClick={onUndo}><Undo2 className="h-4 w-4" /></Button></TooltipTrigger><TooltipContent>{t("diagram.undo")}</TooltipContent></Tooltip>
|
||||
<Tooltip><TooltipTrigger asChild><Button size="icon" variant="ghost" aria-label={t("diagram.redo")} disabled={!canRedo || readOnly} onClick={onRedo}><Redo2 className="h-4 w-4" /></Button></TooltipTrigger><TooltipContent>{t("diagram.redo")}</TooltipContent></Tooltip>
|
||||
{!readOnly && <Tooltip><TooltipTrigger asChild><Button size="icon" variant="ghost" aria-label={t("diagram.deleteSelection")} disabled={!hasSelection} onClick={onDeleteSelection}><Trash2 className="h-4 w-4" /></Button></TooltipTrigger><TooltipContent>{t("diagram.deleteSelection")}</TooltipContent></Tooltip>}
|
||||
{!readOnly && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="sm" variant="ghost" className="gap-1.5 px-2.5" aria-label={t("diagram.autoLayout")} onClick={onAutoLayout}>
|
||||
<WandSparkles className="h-4 w-4" />
|
||||
<span>{t("diagram.autoLayout")}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("diagram.autoLayout")}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<ToolbarDivider />
|
||||
<Tooltip><TooltipTrigger asChild><Button size="icon" variant="ghost" aria-label={t("diagram.zoomOut")} onClick={onZoomOut}><ZoomOut className="h-4 w-4" /></Button></TooltipTrigger><TooltipContent>{t("diagram.zoomOut")}</TooltipContent></Tooltip>
|
||||
<Tooltip><TooltipTrigger asChild><Button size="icon" variant="ghost" aria-label={t("diagram.zoomIn")} onClick={onZoomIn}><ZoomIn className="h-4 w-4" /></Button></TooltipTrigger><TooltipContent>{t("diagram.zoomIn")}</TooltipContent></Tooltip>
|
||||
<Tooltip><TooltipTrigger asChild><Button size="icon" variant="ghost" aria-label={t("diagram.fit")} onClick={onFit}><Maximize2 className="h-4 w-4" /></Button></TooltipTrigger><TooltipContent>{t("diagram.fit")}</TooltipContent></Tooltip>
|
||||
<Select value={theme} disabled={readOnly} onValueChange={(value) => onThemeChange(value as DiagramTheme)}>
|
||||
<SelectTrigger className="h-8 w-[8.5rem] gap-2" aria-label={t("diagram.theme")}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="brand" textValue={t("diagram.themeBrand")}><span className="flex items-center gap-2"><span className="h-3 w-3 rounded-full border border-black/10" style={{ background: resolveDiagramPalette("brand", appearance).topicFill }} />{t("diagram.themeBrand")}</span></SelectItem>
|
||||
<SelectItem value="ocean" textValue={t("diagram.themeOcean")}><span className="flex items-center gap-2"><span className="h-3 w-3 rounded-full border border-black/10" style={{ background: resolveDiagramPalette("ocean", appearance).topicFill }} />{t("diagram.themeOcean")}</span></SelectItem>
|
||||
<SelectItem value="ink" textValue={t("diagram.themeInk")}><span className="flex items-center gap-2"><span className="h-3 w-3 rounded-full border border-black/10" style={{ background: resolveDiagramPalette("ink", appearance).nodeFill }} />{t("diagram.themeInk")}</span></SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="outline"><Download className="h-4 w-4" />{t("diagram.export")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onSelect={() => onExport("png")}><FileImage className="h-4 w-4" />{t("diagram.exportPng")}</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => onExport("svg")}><FileCode2 className="h-4 w-4" />{t("diagram.exportSvg")}</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{selectionEditor}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -351,6 +351,7 @@ export const enUS = {
|
||||
zoomOut: "Zoom out",
|
||||
fit: "Fit diagram",
|
||||
autoLayout: "Auto-arrange",
|
||||
toolbar: "Diagram toolbar",
|
||||
theme: "Diagram theme",
|
||||
themeBrand: "Forest Green",
|
||||
themeOcean: "Mint paper",
|
||||
|
||||
@@ -351,6 +351,7 @@ export const zhCN = {
|
||||
zoomOut: "缩小",
|
||||
fit: "适应画布",
|
||||
autoLayout: "自动整理布局",
|
||||
toolbar: "图形工具栏",
|
||||
theme: "图表主题",
|
||||
themeBrand: "森绿",
|
||||
themeOcean: "薄荷纸",
|
||||
|
||||
Reference in new issue
Block a user