diff --git a/src/App.test.tsx b/src/App.test.tsx
index 673de04..440fa94 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -72,6 +72,27 @@ it("collapses both side panels from the fullscreen toolbar action", async () =>
expect(screen.getByLabelText("属性")).toHaveAttribute("aria-hidden", "true");
});
+it("collapses only one side panel and leaves a handle to bring it back", async () => {
+ const user = userEvent.setup();
+ const { container } = render();
+
+ expect(screen.getByRole("button", { name: "收起场景面板" })).toBeInTheDocument();
+
+ act(() => {
+ useDirectorStore.getState().toggleLeftPanelVisible();
+ });
+
+ expect(container.querySelector(".director-shell-fullbleed.is-left-collapsed")).toBeInTheDocument();
+ expect(screen.getByLabelText("场景")).toHaveAttribute("aria-hidden", "true");
+ // 右侧不受影响
+ expect(screen.getByLabelText("属性")).not.toHaveAttribute("aria-hidden");
+
+ await user.click(screen.getByRole("button", { name: "展开场景面板" }));
+
+ expect(screen.getByLabelText("场景")).not.toHaveAttribute("aria-hidden");
+ expect(container.querySelector(".director-shell-fullbleed.is-left-collapsed")).not.toBeInTheDocument();
+});
+
it("shows the floating timeline by default and hides it from the record action", () => {
const { container } = render();
diff --git a/src/App.tsx b/src/App.tsx
index 182637f..5a1a471 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -41,6 +41,27 @@ export default function App() {
return;
}
+ // 面板显隐: [ 左侧场景栏、] 右侧属性栏、\ 底部时间轴。都不带修饰键,方便单手切换。
+ if (!event.metaKey && !event.ctrlKey && !event.altKey) {
+ if (event.key === "[") {
+ event.preventDefault();
+ useDirectorStore.getState().toggleLeftPanelVisible();
+ return;
+ }
+
+ if (event.key === "]") {
+ event.preventDefault();
+ useDirectorStore.getState().toggleRightPanelVisible();
+ return;
+ }
+
+ if (event.key === "\\") {
+ event.preventDefault();
+ useDirectorStore.getState().toggleTimelineVisible();
+ return;
+ }
+ }
+
if (!event.metaKey && !event.ctrlKey) return;
const key = event.key.toLowerCase();
diff --git a/src/app/layout/DirectorDeskShell.tsx b/src/app/layout/DirectorDeskShell.tsx
index 16bd8ba..424a332 100644
--- a/src/app/layout/DirectorDeskShell.tsx
+++ b/src/app/layout/DirectorDeskShell.tsx
@@ -1,17 +1,36 @@
import type { ReactNode } from "react";
+import { PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from "lucide-react";
import { ObjectTreePanel } from "../../editor/panels/ObjectTreePanel";
import { RightPanel } from "../../editor/panels/RightPanel";
import { TimelinePanel } from "../../editor/panels/TimelinePanel";
import { useDirectorStore } from "../../editor/store/directorStore";
+const HANDLE_ICON_PROPS = { "aria-hidden": true, size: 15, strokeWidth: 2 } as const;
+
export function DirectorDeskShell({ children }: { children: ReactNode }) {
const viewportPanelsCollapsed = useDirectorStore((state) => state.viewportPanelsCollapsed);
+ const leftPanelVisible = useDirectorStore((state) => state.leftPanelVisible);
+ const rightPanelVisible = useDirectorStore((state) => state.rightPanelVisible);
const timelineVisible = useDirectorStore((state) => state.timelineVisible);
+ const toggleLeftPanelVisible = useDirectorStore((state) => state.toggleLeftPanelVisible);
+ const toggleRightPanelVisible = useDirectorStore((state) => state.toggleRightPanelVisible);
+
+ // 「专注模式」是临时一次收起两侧,最终可见性由两侧各自的开关与之相与决定
+ const leftVisible = !viewportPanelsCollapsed && leftPanelVisible;
+ const rightVisible = !viewportPanelsCollapsed && rightPanelVisible;
+
+ const shellClassName = [
+ "director-shell",
+ "director-shell-fullbleed",
+ leftVisible ? "" : "is-left-collapsed",
+ rightVisible ? "" : "is-right-collapsed",
+ !leftVisible && !rightVisible ? "is-sidebars-collapsed" : "",
+ ]
+ .filter(Boolean)
+ .join(" ");
return (
-
+
{children}
@@ -20,18 +39,38 @@ export function DirectorDeskShell({ children }: { children: ReactNode }) {
+
+
);
}
diff --git a/src/editor/canvas/DirectorCanvas.test.tsx b/src/editor/canvas/DirectorCanvas.test.tsx
index 504edde..698a928 100644
--- a/src/editor/canvas/DirectorCanvas.test.tsx
+++ b/src/editor/canvas/DirectorCanvas.test.tsx
@@ -1,5 +1,5 @@
import { forwardRef } from "react";
-import { fireEvent, render, screen, within } from "@testing-library/react";
+import { act, fireEvent, render, screen, within } from "@testing-library/react";
import { afterEach, beforeEach, vi } from "vitest";
import { DoubleSide, PerspectiveCamera, Vector3 } from "three";
import { clearViewportCaptureHandler, requestViewportCapture } from "../io/captureBridge";
@@ -484,6 +484,32 @@ it("offsets the native viewport gizmo inward when overlay side panels are open",
});
});
+it("pulls the viewport overlays to the edge when only the right panel is collapsed", () => {
+ act(() => {
+ useDirectorStore.setState({
+ ...useDirectorStore.getState(),
+ viewMode: "director",
+ viewportPanelsCollapsed: false,
+ leftPanelVisible: true,
+ rightPanelVisible: true,
+ });
+ });
+
+ render();
+
+ expect(screen.getByLabelText("3D视口原生坐标控件")).toHaveStyle({ right: "332px" });
+
+ act(() => {
+ useDirectorStore.getState().toggleRightPanelVisible();
+ });
+
+ // 右侧面板收起后这一侧整块让给画布:坐标控件 / 重置视角 / 镜头监视器都要贴到边缘,
+ // 不能只看「专注模式」总开关
+ expect(screen.getByLabelText("3D视口原生坐标控件")).toHaveStyle({ right: "20px" });
+ expect(document.querySelector(".viewport-view-reset")).toHaveStyle({ right: "20px" });
+ expect(screen.getByLabelText("镜头监视器")).toHaveStyle({ right: "20px" });
+});
+
it("resets the director view from the button below the native gizmo", () => {
useDirectorStore.getState().setViewMode("camera");
render();
diff --git a/src/editor/canvas/DirectorCanvas.tsx b/src/editor/canvas/DirectorCanvas.tsx
index a9a013d..8646d9e 100644
--- a/src/editor/canvas/DirectorCanvas.tsx
+++ b/src/editor/canvas/DirectorCanvas.tsx
@@ -935,6 +935,8 @@ export function DirectorCanvas() {
const viewportAspectRatio = useDirectorStore((state) => state.viewportAspectRatio);
const viewportRuleOfThirdsEnabled = useDirectorStore((state) => state.viewportRuleOfThirdsEnabled);
const viewportPanelsCollapsed = useDirectorStore((state) => state.viewportPanelsCollapsed);
+ const leftPanelVisible = useDirectorStore((state) => state.leftPanelVisible);
+ const rightPanelVisible = useDirectorStore((state) => state.rightPanelVisible);
const trajectoryPlacementObjectId = useDirectorStore((state) => state.trajectoryPlacementObjectId);
const setViewMode = useDirectorStore((state) => state.setViewMode);
const setViewportRuleOfThirdsEnabled = useDirectorStore((state) => state.setViewportRuleOfThirdsEnabled);
@@ -947,17 +949,18 @@ export function DirectorCanvas() {
target: getCameraLookPoint(activeCameraWorldView.position, activeCameraWorldView.lookRotation),
}
: directorViewSnapshot;
- const viewportSafeAreaInsets: ViewportSafeAreaInsets = viewportPanelsCollapsed
- ? { left: 0, right: 0, top: 0, bottom: 0 }
- : {
- left: LEFT_PANEL_WIDTH + FLOATING_PANEL_GAP,
- right: RIGHT_PANEL_WIDTH + FLOATING_PANEL_GAP,
- top: 0,
- bottom: 0,
- };
- const gizmoRightOffset = viewportPanelsCollapsed
- ? GIZMO_EDGE_PADDING
- : RIGHT_PANEL_WIDTH + FLOATING_PANEL_GAP;
+ // 某一侧面板收起后,画布这一侧就整块让出来了:坐标控件 / 重置视角 / 镜头监视器、
+ // 以及画幅安全区都得跟着贴到新的画布边缘。判据是「这一侧是否真的显示」,
+ // 不能只看「专注模式」总开关 —— 单独按 [ / ] 收起一侧时总开关并没有变。
+ const leftPanelShown = !viewportPanelsCollapsed && leftPanelVisible;
+ const rightPanelShown = !viewportPanelsCollapsed && rightPanelVisible;
+ const viewportSafeAreaInsets: ViewportSafeAreaInsets = {
+ left: leftPanelShown ? LEFT_PANEL_WIDTH + FLOATING_PANEL_GAP : 0,
+ right: rightPanelShown ? RIGHT_PANEL_WIDTH + FLOATING_PANEL_GAP : 0,
+ top: 0,
+ bottom: 0,
+ };
+ const gizmoRightOffset = rightPanelShown ? RIGHT_PANEL_WIDTH + FLOATING_PANEL_GAP : GIZMO_EDGE_PADDING;
useLayoutEffect(() => {
const element = toolbarRef.current;
diff --git a/src/editor/canvas/ViewportToolbar.test.tsx b/src/editor/canvas/ViewportToolbar.test.tsx
index 2a42446..d786cef 100644
--- a/src/editor/canvas/ViewportToolbar.test.tsx
+++ b/src/editor/canvas/ViewportToolbar.test.tsx
@@ -71,7 +71,7 @@ it("renders the viewport capsule as project icon-system buttons", () => {
"当前视角截图",
"四方位截图",
"十二方位截图",
- "全屏",
+ "专注模式",
"添加轨迹",
"录制",
];
@@ -113,7 +113,7 @@ it("uses the requested viewport toolbar SVG icons for camera and capture actions
expect(screen.getByRole("button", { name: "十二方位截图" }).querySelector("svg")).toHaveClass("lucide-grid3x3");
});
-it("uses the fullscreen button to collapse the side panels instead of entering browser fullscreen", async () => {
+it("uses the focus-mode button to collapse the side panels instead of entering browser fullscreen", async () => {
const user = userEvent.setup();
render();
@@ -121,7 +121,7 @@ it("uses the fullscreen button to collapse the side panels instead of entering b
false
);
- await user.click(screen.getByRole("button", { name: "全屏" }));
+ await user.click(screen.getByRole("button", { name: "专注模式" }));
expect((useDirectorStore.getState() as { viewportPanelsCollapsed?: boolean }).viewportPanelsCollapsed ?? false).toBe(
true
diff --git a/src/editor/canvas/ViewportToolbar.tsx b/src/editor/canvas/ViewportToolbar.tsx
index 3e354f4..97d8d8a 100644
--- a/src/editor/canvas/ViewportToolbar.tsx
+++ b/src/editor/canvas/ViewportToolbar.tsx
@@ -143,7 +143,11 @@ export function ViewportToolbar({
const setViewMode = useDirectorStore((state) => state.setViewMode);
const setTransformMode = useDirectorStore((state) => state.setTransformMode);
const setViewportAspectRatio = useDirectorStore((state) => state.setViewportAspectRatio);
- const toggleViewportPanelsCollapsed = useDirectorStore((state) => state.toggleViewportPanelsCollapsed);
+ const setViewportPanelsCollapsed = useDirectorStore((state) => state.setViewportPanelsCollapsed);
+ // 左右侧栏是否都不见了:既可能是「专注模式」,也可能是被 [ / ] 各自收起
+ const sidePanelsHidden = useDirectorStore(
+ (state) => state.viewportPanelsCollapsed || (!state.leftPanelVisible && !state.rightPanelVisible)
+ );
const project = useDirectorStore((state) => state.project);
const selectedObjectId = useDirectorStore((state) => state.selectedObjectId);
const timelineVisible = useDirectorStore((state) => state.timelineVisible);
@@ -511,7 +515,15 @@ export function ViewportToolbar({
{ label: "当前视角截图", icon: ScanLine, onClick: () => void handleCapture("current") },
{ label: "四方位截图", icon: Grid2X2, onClick: () => void handleCapture("four") },
{ label: "十二方位截图", icon: Grid3X3, onClick: () => void handleCapture("twelve") },
- { label: "全屏", icon: Expand, onClick: toggleViewportPanelsCollapsed },
+ {
+ label: "专注模式",
+ icon: Expand,
+ pressed: sidePanelsHidden,
+ title: sidePanelsHidden
+ ? "恢复左右面板(可用 [ 左侧 / ] 右侧 单独切换一侧)"
+ : "专注模式:一次收起左右面板,只看画布(可用 [ 左侧 / ] 右侧 单独切换一侧)",
+ onClick: () => setViewportPanelsCollapsed(!sidePanelsHidden),
+ },
// 打点 / 录制放在最右侧
{
label: "添加轨迹",
diff --git a/src/editor/store/directorStore.test.ts b/src/editor/store/directorStore.test.ts
index 0e3d52a..b342307 100644
--- a/src/editor/store/directorStore.test.ts
+++ b/src/editor/store/directorStore.test.ts
@@ -91,6 +91,30 @@ it("toggles the viewport side panel collapse flag in ui state", () => {
expect((useDirectorStore.getState() as CollapseUiState).viewportPanelsCollapsed ?? false).toBe(true);
});
+it("toggles the left and right side panels independently", () => {
+ useDirectorStore.setState(createInitialDirectorState());
+
+ useDirectorStore.getState().toggleLeftPanelVisible();
+
+ const state = useDirectorStore.getState();
+ expect(state.leftPanelVisible).toBe(false);
+ // 只收起左侧,右侧不受影响
+ expect(state.rightPanelVisible).toBe(true);
+ // 单侧收起不改变「专注模式」标记
+ expect(state.viewportPanelsCollapsed).toBe(false);
+});
+
+it("leaves focus mode and keeps only the toggled side when a panel switch is used", () => {
+ useDirectorStore.setState({ ...createInitialDirectorState(), viewportPanelsCollapsed: true });
+
+ useDirectorStore.getState().toggleRightPanelVisible();
+
+ const state = useDirectorStore.getState();
+ expect(state.viewportPanelsCollapsed).toBe(false);
+ expect(state.leftPanelVisible).toBe(false);
+ expect(state.rightPanelVisible).toBe(true);
+});
+
it("routes the right panel by object type and view mode", () => {
const state = createInitialDirectorState();
const characterId = state.project.objects.find((item) => item.kind === "character")!.id;
diff --git a/src/editor/store/directorStore.ts b/src/editor/store/directorStore.ts
index 698aa64..bbe25b4 100644
--- a/src/editor/store/directorStore.ts
+++ b/src/editor/store/directorStore.ts
@@ -140,7 +140,12 @@ export interface DirectorUiState {
transformMode: TransformMode;
viewportAspectRatio: ViewportAspectRatio;
viewportRuleOfThirdsEnabled: boolean;
+ /** 「专注模式」:一次性收起左右两侧栏。与左右各自的显隐开关相与后决定是否渲染。 */
viewportPanelsCollapsed: boolean;
+ /** 左侧「场景」面板是否显示(专注模式下会被一并收起) */
+ leftPanelVisible: boolean;
+ /** 右侧「属性」面板是否显示(专注模式下会被一并收起) */
+ rightPanelVisible: boolean;
/** 悬浮时间轴(录制面板)是否显示,由视口快捷工具条的「录制」按钮切换 */
timelineVisible: boolean;
/** 用户自定义的时间轴轨道顺序(轨道 key 列表)。
@@ -183,6 +188,10 @@ export interface DirectorActions {
setViewportRuleOfThirdsEnabled: (enabled: boolean) => void;
toggleViewportPanelsCollapsed: () => void;
setViewportPanelsCollapsed: (collapsed: boolean) => void;
+ /** 单独收起 / 展开左侧「场景」面板 */
+ toggleLeftPanelVisible: () => void;
+ /** 单独收起 / 展开右侧「属性」面板 */
+ toggleRightPanelVisible: () => void;
toggleTimelineVisible: () => void;
setTimelineVisible: (visible: boolean) => void;
/** 覆盖时间轴轨道顺序(传入解析后的完整顺序) */
@@ -435,6 +444,8 @@ const DEFAULT_UI_STATE: DirectorUiState = {
viewportAspectRatio: "auto",
viewportRuleOfThirdsEnabled: false,
viewportPanelsCollapsed: false,
+ leftPanelVisible: true,
+ rightPanelVisible: true,
timelineVisible: true,
timelineTrackOrder: [],
timelineCollapsedTracks: [],
@@ -917,6 +928,8 @@ function extractPersistedDirectorState(state: DirectorRuntimeState): DirectorSta
viewportAspectRatio: state.viewportAspectRatio,
viewportRuleOfThirdsEnabled: state.viewportRuleOfThirdsEnabled,
viewportPanelsCollapsed: state.viewportPanelsCollapsed,
+ leftPanelVisible: state.leftPanelVisible,
+ rightPanelVisible: state.rightPanelVisible,
timelineVisible: state.timelineVisible,
timelineTrackOrder: state.timelineTrackOrder,
timelineCollapsedTracks: state.timelineCollapsedTracks,
@@ -981,6 +994,8 @@ function readPersistedDirectorState(options: DirectorStateOptions = {}): Directo
viewportAspectRatio: state.viewportAspectRatio ?? "auto",
viewportRuleOfThirdsEnabled: Boolean(state.viewportRuleOfThirdsEnabled),
viewportPanelsCollapsed: Boolean(state.viewportPanelsCollapsed),
+ leftPanelVisible: state.leftPanelVisible === false ? false : true,
+ rightPanelVisible: state.rightPanelVisible === false ? false : true,
timelineVisible: state.timelineVisible === false ? false : true,
timelineTrackOrder: readPersistedStringArray(state.timelineTrackOrder),
timelineCollapsedTracks: readPersistedStringArray(state.timelineCollapsedTracks),
@@ -2160,7 +2175,27 @@ export const useDirectorStore = create((set, get) => {
commitUiMutation((state) => ({
...state,
viewportPanelsCollapsed: collapsed,
+ // 退出专注模式时把两侧还原成可见,避免「点了展开却没反应」
+ leftPanelVisible: collapsed ? state.leftPanelVisible : true,
+ rightPanelVisible: collapsed ? state.rightPanelVisible : true,
})),
+ toggleLeftPanelVisible: () =>
+ commitUiMutation((state) => {
+ // 专注模式下切换某一侧:退出专注模式,只保留这一侧,避免「按了没反应」
+ if (state.viewportPanelsCollapsed) {
+ return { ...state, viewportPanelsCollapsed: false, leftPanelVisible: true, rightPanelVisible: false };
+ }
+
+ return { ...state, leftPanelVisible: !state.leftPanelVisible };
+ }),
+ toggleRightPanelVisible: () =>
+ commitUiMutation((state) => {
+ if (state.viewportPanelsCollapsed) {
+ return { ...state, viewportPanelsCollapsed: false, leftPanelVisible: false, rightPanelVisible: true };
+ }
+
+ return { ...state, rightPanelVisible: !state.rightPanelVisible };
+ }),
toggleTimelineVisible: () =>
commitUiMutation((state) => ({
...state,
diff --git a/src/styles/index.css b/src/styles/index.css
index bd781fd..09c7e3e 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -18,10 +18,11 @@
--right-sidebar-width: 300px;
--left-sidebar-content-width: 260px;
--right-sidebar-content-width: 260px;
- /* 悬浮卡片:与视口边缘的间距、以及让开顶部工具条的起始高度 */
+ /* 悬浮卡片与视口边缘的间距(左右、以及相对时间轴的上下留白都用它),
+ 卡片顶部同样取这个值,四周留白保持一致 */
--floating-inset: 16px;
- --floating-top: 76px;
- /* 悬浮卡片可用高度上限:扣掉顶部工具条、上下内缩、以及悬浮时间轴占用的高度 */
+ --floating-top: var(--floating-inset);
+ /* 悬浮卡片可用高度上限:扣掉卡片起始高度、上下内缩、以及悬浮时间轴占用的高度 */
--floating-card-max-height: calc(100vh - var(--floating-top) - var(--floating-inset) * 2 - var(--timeline-panel-height, 0px));
/* 视口快捷工具条高度(ViewportToolbar 会按实测值覆盖),供悬浮时间轴让位使用 */
--viewport-toolbar-height: 46px;
@@ -379,7 +380,9 @@ textarea:focus-visible {
}
.director-shell-fullbleed.is-sidebars-collapsed > .left-sidebar,
-.director-shell-fullbleed.is-sidebars-collapsed > .right-sidebar {
+.director-shell-fullbleed.is-sidebars-collapsed > .right-sidebar,
+.director-shell-fullbleed.is-left-collapsed > .left-sidebar,
+.director-shell-fullbleed.is-right-collapsed > .right-sidebar {
display: none;
}
@@ -3576,6 +3579,15 @@ button.inspector-dropdown-option.is-selected {
display: none;
}
+ /* 窄屏下右侧栏本就强制隐藏,连它的手柄一起去掉,免得出现「点了没反应」的控件 */
+ .panel-edge-handle-right {
+ display: none;
+ }
+
+ .panel-edge-handle-left {
+ left: calc(var(--floating-inset) + min(220px, 28vw) - 11px);
+ }
+
.viewport-toolbar {
max-width: calc(100% - 32px);
}
@@ -5035,9 +5047,9 @@ body.is-timeline-dragging * {
}
/* ===== 全屏画布 + 悬浮卡片 =====
- 画布铺满整屏;顶部工具条、左右侧栏、底部时间轴一律脱离文档流悬浮在画布之上。
- 1) 顶部工具条改为绝对定位的透明浮层,只有标题/视角切换/关闭按钮可点击,其余区域让给画布。
- 2) 右侧栏脱离第 1 行,直接落在 1fr 行上,从而占满整屏高度。 */
+ 画布铺满整屏;顶部工具条、左右侧栏、底部时间轴一律脱离文档流悬浮在画布之上。
+ 1) 顶部工具条改为绝对定位的透明浮层,只有视角切换按钮可点击,其余区域让给画布。
+ 2) 右侧栏脱离第 1 行,直接落在 1fr 行上,从而占满整屏高度。 */
.app-shell > .director-shell {
grid-row: 2;
}
@@ -5053,8 +5065,13 @@ body.is-timeline-dragging * {
pointer-events: none;
}
-.top-bar-left,
+/* 只有视角切换按钮需要接管点击:整格可点会连带吃掉画布顶部中段、
+ 以及延伸到顶部的侧栏边缘,所以这里收到按钮本身 */
.top-bar-center {
+ pointer-events: none;
+}
+
+.top-bar-center .mode-toggle {
pointer-events: auto;
}
@@ -5066,7 +5083,7 @@ body.is-timeline-dragging * {
-webkit-backdrop-filter: blur(32px);
}
-/* 左右侧栏:从贴边全高面板改成内缩、按内容包裹的圆角悬浮卡片
+/* 左右侧栏:从贴边全高面板改成内缩、按内容包裹的悬浮卡片,顶部延伸到视口顶端
bottom 置为 auto + max-height 封顶 → 内容少时卡片只有内容那么高,内容多时卡片内部滚动 */
.director-shell-fullbleed .left-sidebar,
.director-shell-fullbleed .right-sidebar {
@@ -5096,6 +5113,53 @@ body.is-timeline-dragging * {
right: var(--floating-inset);
}
+/* 侧栏折叠手柄:展开时半嵌在卡片外缘(落在卡片 20px 内边距里,不压内容),
+ 收起后缩到视口边上,只占一条 22px 的窄边,把画布整块让出来。
+ z-index 高于侧栏(25) / 时间轴(30),低于顶部条(45)。 */
+.panel-edge-handle {
+ position: absolute;
+ top: calc(var(--floating-top) + 18px);
+ z-index: 40;
+ display: grid;
+ place-items: center;
+ width: 22px;
+ height: 48px;
+ padding: 0;
+ border: 1px solid rgb(var(--border-rgb) / 0.42);
+ border-radius: 999px;
+ color: rgb(var(--text-muted-rgb));
+ background: rgb(var(--panel-rgb) / 0.9);
+ box-shadow: var(--ui-shadow-panel);
+ backdrop-filter: blur(32px);
+ -webkit-backdrop-filter: blur(32px);
+ cursor: pointer;
+ transition:
+ color 0.15s ease,
+ background 0.15s ease;
+}
+
+.panel-edge-handle:hover,
+.panel-edge-handle:focus-visible {
+ color: rgb(var(--text-rgb));
+ background: rgb(var(--panel-rgb) / 0.98);
+}
+
+.panel-edge-handle-left {
+ left: calc(var(--floating-inset) + var(--left-sidebar-width) - 11px);
+}
+
+.panel-edge-handle-right {
+ right: calc(var(--floating-inset) + var(--right-sidebar-width) - 11px);
+}
+
+.director-shell-fullbleed.is-left-collapsed > .panel-edge-handle-left {
+ left: 8px;
+}
+
+.director-shell-fullbleed.is-right-collapsed > .panel-edge-handle-right {
+ right: 8px;
+}
+
/* 卡片自身半透明,内部面板背景需让位,避免把悬浮磨砂底压成不透明;
同时把内层依赖父级高度的 100% 显式改成跟随内容,配合卡片「按内容包裹」 */
.director-shell-fullbleed .left-sidebar .object-tree-panel {