feat(viewport): 支持左右侧栏独立折叠与专注模式
- 新增 leftPanelVisible / rightPanelVisible 独立状态,可与专注模式叠加 - 添加侧栏折叠手柄及 [ / ] / \ 快捷键 - 画布安全区与悬浮控件按单侧可见性动态贴合边缘 - 顶部工具条「全屏」按钮更名为「专注模式」并更新提示 - 调整悬浮卡片顶部留白,统一视口四周间距
This commit is contained in:
@@ -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(<App />);
|
||||
|
||||
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(<App />);
|
||||
|
||||
|
||||
21
src/App.tsx
21
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();
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={`director-shell director-shell-fullbleed${viewportPanelsCollapsed ? " is-sidebars-collapsed" : ""}`}
|
||||
>
|
||||
<div className={shellClassName}>
|
||||
<section className="viewport-column" aria-label="3D视口">
|
||||
<div className="viewport-stack">
|
||||
{children}
|
||||
@@ -20,18 +39,38 @@ export function DirectorDeskShell({ children }: { children: ReactNode }) {
|
||||
</section>
|
||||
<aside
|
||||
className="left-sidebar director-sidebar"
|
||||
aria-hidden={viewportPanelsCollapsed ? "true" : undefined}
|
||||
aria-hidden={leftVisible ? undefined : "true"}
|
||||
aria-label="场景"
|
||||
>
|
||||
<ObjectTreePanel />
|
||||
</aside>
|
||||
<aside
|
||||
className="right-sidebar director-sidebar"
|
||||
aria-hidden={viewportPanelsCollapsed ? "true" : undefined}
|
||||
aria-hidden={rightVisible ? undefined : "true"}
|
||||
aria-label="属性"
|
||||
>
|
||||
<RightPanel />
|
||||
</aside>
|
||||
<button
|
||||
aria-expanded={leftVisible}
|
||||
aria-label={leftVisible ? "收起场景面板" : "展开场景面板"}
|
||||
className="panel-edge-handle panel-edge-handle-left"
|
||||
title={leftVisible ? "收起场景面板([)" : "展开场景面板([)"}
|
||||
type="button"
|
||||
onClick={toggleLeftPanelVisible}
|
||||
>
|
||||
{leftVisible ? <PanelLeftClose {...HANDLE_ICON_PROPS} /> : <PanelLeftOpen {...HANDLE_ICON_PROPS} />}
|
||||
</button>
|
||||
<button
|
||||
aria-expanded={rightVisible}
|
||||
aria-label={rightVisible ? "收起属性面板" : "展开属性面板"}
|
||||
className="panel-edge-handle panel-edge-handle-right"
|
||||
title={rightVisible ? "收起属性面板(])" : "展开属性面板(])"}
|
||||
type="button"
|
||||
onClick={toggleRightPanelVisible}
|
||||
>
|
||||
{rightVisible ? <PanelRightClose {...HANDLE_ICON_PROPS} /> : <PanelRightOpen {...HANDLE_ICON_PROPS} />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(<App />);
|
||||
|
||||
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(<App />);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(<ViewportToolbar />);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: "添加轨迹",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<DirectorStore>((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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user