源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { JSX, SVGProps } from "react";
|
||||
import { GridBottomLinedIcon } from "../../../app/renderer/components/icons/GridBottomLinedIcon";
|
||||
import { GridLeftLinedIcon } from "../../../app/renderer/components/icons/GridLeftLinedIcon";
|
||||
import { GridRightLinedIcon } from "../../../app/renderer/components/icons/GridRightLinedIcon";
|
||||
import { GridTopLinedIcon } from "../../../app/renderer/components/icons/GridTopLinedIcon";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger
|
||||
} from "@tutti-os/ui-system";
|
||||
import { Button } from "../../../app/renderer/components/ui/button";
|
||||
import { useTranslation } from "../../../i18n/index";
|
||||
import { CanvasNodeGhostIconButton } from "../../shared/CanvasNodeGhostIconButton";
|
||||
import {
|
||||
CanvasNodeLayoutLinedIcon,
|
||||
CanvasNodeMaximizeLinedIcon,
|
||||
CanvasNodeMinimizeLinedIcon
|
||||
} from "../../shared/canvasNodeChromeIcons";
|
||||
import { useDesktopStore } from "../store";
|
||||
import type { DesktopSize, WindowQuickLayoutTarget } from "../types";
|
||||
|
||||
const QUICK_LAYOUT_OPTIONS: Array<{
|
||||
target: WindowQuickLayoutTarget;
|
||||
icon: (props: SVGProps<SVGSVGElement>) => JSX.Element;
|
||||
labelKey: string;
|
||||
}> = [
|
||||
{
|
||||
target: "left",
|
||||
icon: GridLeftLinedIcon,
|
||||
labelKey: "workspaceWindowLayout.left"
|
||||
},
|
||||
{
|
||||
target: "right",
|
||||
icon: GridRightLinedIcon,
|
||||
labelKey: "workspaceWindowLayout.right"
|
||||
},
|
||||
{
|
||||
target: "top",
|
||||
icon: GridTopLinedIcon,
|
||||
labelKey: "workspaceWindowLayout.top"
|
||||
},
|
||||
{
|
||||
target: "bottom",
|
||||
icon: GridBottomLinedIcon,
|
||||
labelKey: "workspaceWindowLayout.bottom"
|
||||
}
|
||||
];
|
||||
|
||||
const WINDOW_LAYOUT_MENU_HOVER_OPEN_DELAY_MS = 300;
|
||||
|
||||
interface WindowLayoutMenuButtonProps {
|
||||
windowId: string;
|
||||
desktopSize: DesktopSize;
|
||||
className?: string;
|
||||
"data-window-header"?: string;
|
||||
}
|
||||
|
||||
export function WindowLayoutMenuButton({
|
||||
windowId,
|
||||
desktopSize,
|
||||
className,
|
||||
"data-window-header": windowHeader
|
||||
}: WindowLayoutMenuButtonProps): JSX.Element {
|
||||
"use memo";
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const hoverOpenTimeoutRef = useRef<number | null>(null);
|
||||
const applyQuickLayout = useDesktopStore((state) => state.applyQuickLayout);
|
||||
const enterFullscreenWindow = useDesktopStore(
|
||||
(state) => state.enterFullscreenWindow
|
||||
);
|
||||
const exitFullscreenWindow = useDesktopStore(
|
||||
(state) => state.exitFullscreenWindow
|
||||
);
|
||||
const isFullscreen = useDesktopStore(
|
||||
(state) =>
|
||||
state.windows.find((window) => window.id === windowId)?.displayMode ===
|
||||
"fullscreen"
|
||||
);
|
||||
const fullscreenLabelKey = isFullscreen
|
||||
? "workspaceWindowLayout.restore"
|
||||
: "workspaceWindowLayout.fullscreen";
|
||||
const fullscreenIconState = isFullscreen ? "restore" : "fullscreen";
|
||||
const FullscreenIcon = isFullscreen
|
||||
? CanvasNodeMinimizeLinedIcon
|
||||
: CanvasNodeMaximizeLinedIcon;
|
||||
|
||||
const applyLayout = (target: WindowQuickLayoutTarget) => {
|
||||
applyQuickLayout(windowId, target, desktopSize);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const clearHoverOpenTimeout = () => {
|
||||
if (hoverOpenTimeoutRef.current !== null) {
|
||||
window.clearTimeout(hoverOpenTimeoutRef.current);
|
||||
hoverOpenTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleHoverOpen = () => {
|
||||
if (open || hoverOpenTimeoutRef.current !== null) {
|
||||
return;
|
||||
}
|
||||
hoverOpenTimeoutRef.current = window.setTimeout(() => {
|
||||
hoverOpenTimeoutRef.current = null;
|
||||
setOpen(true);
|
||||
}, WINDOW_LAYOUT_MENU_HOVER_OPEN_DELAY_MS);
|
||||
};
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (isFullscreen) {
|
||||
exitFullscreenWindow(windowId, desktopSize);
|
||||
} else {
|
||||
enterFullscreenWindow(windowId, desktopSize);
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => clearHoverOpenTimeout, []);
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
if (!nextOpen) {
|
||||
clearHoverOpenTimeout();
|
||||
}
|
||||
setOpen(nextOpen);
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<CanvasNodeGhostIconButton
|
||||
className={className}
|
||||
data-window-header={windowHeader}
|
||||
data-window-layout-menu-trigger="true"
|
||||
data-menu-open={open ? "true" : undefined}
|
||||
data-testid="window-layout-menu-trigger"
|
||||
aria-label={t("workspaceWindowLayout.trigger")}
|
||||
title={t("workspaceWindowLayout.trigger")}
|
||||
onPointerEnter={scheduleHoverOpen}
|
||||
onPointerLeave={clearHoverOpenTimeout}
|
||||
>
|
||||
<CanvasNodeLayoutLinedIcon
|
||||
width={18}
|
||||
height={18}
|
||||
aria-hidden="true"
|
||||
data-testid="window-layout-menu-trigger-icon"
|
||||
/>
|
||||
</CanvasNodeGhostIconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
sideOffset={10}
|
||||
className="t-dropdown is-open relative min-w-[160px] rounded-[var(--tutti-radius-xl)] border border-hairline bg-background-fronted p-0 text-foreground shadow-[var(--tsh-shell-shadow)]"
|
||||
data-origin="top-right"
|
||||
data-testid="window-layout-menu"
|
||||
>
|
||||
<div className="p-2">
|
||||
<div className="mx-1 mt-0.5 mb-1.5 px-1 text-[13px] leading-[20px] font-semibold text-[var(--text-primary)]">
|
||||
{t("workspaceWindowLayout.moveAndResize")}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-1">
|
||||
{QUICK_LAYOUT_OPTIONS.map((option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<Button
|
||||
key={option.target}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="iconClose"
|
||||
aria-label={t(option.labelKey)}
|
||||
title={t(option.labelKey)}
|
||||
data-testid={`window-layout-menu-quick-layout-${option.target}`}
|
||||
onClick={() => applyLayout(option.target)}
|
||||
>
|
||||
<Icon width={16} height={16} aria-hidden="true" />
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-1.5 my-1 border-t border-[var(--line-2)]" />
|
||||
<div className="px-1">
|
||||
<button
|
||||
className="inline-flex min-h-8 w-full items-center gap-2 whitespace-nowrap rounded-[6px] border border-transparent bg-transparent px-0 text-left text-[13px] leading-[20px] font-semibold text-[var(--text-primary)] transition-[background-color,color,border-color] duration-150 ease-in-out hover:bg-block focus-visible:bg-block focus-visible:outline-none"
|
||||
type="button"
|
||||
aria-label={t(fullscreenLabelKey)}
|
||||
title={t(fullscreenLabelKey)}
|
||||
onClick={toggleFullscreen}
|
||||
>
|
||||
<FullscreenIcon
|
||||
width={16}
|
||||
height={16}
|
||||
aria-hidden="true"
|
||||
data-testid="window-layout-menu-fullscreen-icon"
|
||||
data-window-layout-fullscreen-icon={fullscreenIconState}
|
||||
className="shrink-0"
|
||||
/>
|
||||
<span className="whitespace-nowrap">{t(fullscreenLabelKey)}</span>
|
||||
</button>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user