fix(workbench): show splash-style progress when opening projects

Цей коміт міститься в:
l0ng-ai committed 2026-11-04 09:39:32 +08:00
1 джерело 770178966f
коміт 4d086c44c9
11 files changed
+320 -27

No files matched your search

+85
Переглянути файл
@@ -1,4 +1,5 @@
.workbench {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
@@ -6,6 +7,90 @@
background: var(--bg-app);
color: var(--fg);
}
.content {
display: contents;
}
.content[inert] {
visibility: hidden;
}
.opening {
position: absolute;
inset: 0;
z-index: var(--z-panel);
display: flex;
flex-direction: column;
background: var(--bg-frame);
}
.openingBody {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--splash-gap);
padding: var(--space-4) var(--space-4) var(--splash-offset);
color: var(--fg-muted);
text-align: center;
animation: opening-in var(--dur-slow) ease-out both;
}
.openingBrand {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--splash-gap);
font-size: var(--fs-input-large);
font-weight: 500;
line-height: 1.21;
}
.openingLogo {
width: var(--splash-logo-size);
height: var(--splash-logo-size);
}
.openingProgress {
width: var(--splash-bar-width);
height: var(--splash-bar-height);
flex-shrink: 0;
border-radius: calc(var(--splash-bar-height) / 2);
background: color-mix(in srgb, var(--fg-muted) 24%, transparent);
}
.opening .openingProgress > div {
left: 0;
width: 100%;
opacity: 1;
background: var(--accent);
transform-origin: left;
animation: opening-progress var(--splash-progress-duration) var(--splash-progress-easing) both;
}
.openingTitle {
max-width: 100%;
overflow-wrap: anywhere;
font-size: var(--fs-small);
}
.openingPath {
max-width: 100%;
overflow-wrap: anywhere;
font-size: var(--fs-tiny);
}
@keyframes opening-in {
from {
opacity: 0;
}
}
@keyframes opening-progress {
from {
transform: scaleX(0.04);
}
to {
transform: scaleX(0.92);
}
}
@media (prefers-reduced-motion: reduce) {
.openingBody {
animation: none;
}
}
/* Islands frame; the project colour washes in from the top-left corner (IDEA project gradient). */
.framed {
background:
+35
Переглянути файл
@@ -325,6 +325,41 @@ describe("tool window split areas", () => {
});
describe("workbench shell", () => {
it("covers the old project with the opening target and keeps its views mounted and inert", async () => {
const { useProject } = await import("@/platform/project");
const { Workbench } = await import("./Workbench");
const dispose = registerToolWindow({
id: "project",
title: "Project",
icon: "folder",
anchor: "left",
component: () => <input aria-label="old project input" />,
});
useProject.setState({
project: { root: "/old", name: "old", trusted: true, isGit: false, settings: {} },
opening: false,
openingPath: null,
});
loadToolWindowLayout("/old");
const { unmount } = render(<Workbench />);
const input = screen.getByRole("textbox", { name: "old project input" });
try {
act(() => useProject.setState({ opening: true, openingPath: "/work/new-project" }));
expect(screen.getByRole("status", { name: "Opening new-project…" })).toBeVisible();
expect(screen.getByText("/work/new-project")).toBeVisible();
expect(input.closest("[inert]")).not.toBeNull();
expect(input).toBeInTheDocument();
act(() => useProject.setState({ opening: false, openingPath: null }));
expect(screen.queryByText("Opening new-project…")).toBeNull();
expect(input.closest("[inert]")).toBeNull();
expect(screen.getByRole("textbox", { name: "old project input" })).toBe(input);
} finally {
unmount();
dispose();
useProject.setState({ project: null, opening: false, openingPath: null });
}
});
it("renders the welcome view without a project and the frame with one", async () => {
const { useProject } = await import("@/platform/project");
const { Workbench } = await import("./Workbench");
+44 -16
Переглянути файл
@@ -6,6 +6,9 @@
*/
import { useEditorRegistry } from "@/editor/api";
import { basename, useProject } from "@/platform/project";
import { useT } from "@/platform/i18n";
import { ProgressBar } from "@/ui";
import projectMark from "../../src-tauri/icons/source/openidea-mark.svg";
import { useDistractionFree } from "./distractionFree";
import { EditorArea } from "./EditorArea";
import { OverlayHost } from "./OverlayHost";
@@ -37,11 +40,33 @@ function LightEditFrame() {
);
}
function ProjectOpening({ path }: { path: string }) {
const t = useT();
const label = t("app.project.opening", { name: basename(path) });
return (
<div className={styles.opening} data-project-opening>
<TitleBar bare />
<div className={styles.openingBody} role="status" aria-label={label}>
<div className={styles.openingBrand} aria-hidden="true">
<img className={styles.openingLogo} src={projectMark} alt="" draggable={false} />
<span>OpenIDEA</span>
</div>
<ProgressBar className={styles.openingProgress} aria-label={label} />
<div className={styles.openingTitle}>{label}</div>
<div className={styles.openingPath} title={path}>
{path}
</div>
</div>
</div>
);
}
export function Workbench() {
useApplyTheme();
const banners = useContributions((s) => s.workbenchBanners);
const project = useProject((s) => s.project);
const lightEdit = useProject((s) => s.lightEdit);
const openingPath = useProject((s) => s.openingPath);
const root = project?.root ?? null;
const distractionFree = useDistractionFree((s) => s.active);
const frame = project ? styles.framed : lightEdit ? styles.lightEditFrame : undefined;
@@ -51,22 +76,25 @@ export function Workbench() {
className={frame ? `${styles.workbench} ${frame}` : styles.workbench}
style={project ? projectColorVars(project.name) : undefined}
>
{project ? (
<>
{/* Distraction Free Mode keeps the bar for the traffic lights and window dragging only. */}
<TitleBar bare={distractionFree} />
{!distractionFree && <WindowGroupTabs />}
{[...banners.values()].map(({ id, component: Banner }) => (
<Banner key={id} />
))}
<ToolWindowLayout key={root} editor={<EditorArea />} />
{!distractionFree && <StatusBar />}
</>
) : lightEdit ? (
<LightEditFrame />
) : (
<Welcome />
)}
<div className={styles.content} inert={openingPath !== null} aria-hidden={openingPath ? true : undefined}>
{project ? (
<>
{/* Distraction Free Mode keeps the bar for the traffic lights and window dragging only. */}
<TitleBar bare={distractionFree} />
{!distractionFree && <WindowGroupTabs />}
{[...banners.values()].map(({ id, component: Banner }) => (
<Banner key={id} />
))}
<ToolWindowLayout key={root} editor={<EditorArea />} />
{!distractionFree && <StatusBar />}
</>
) : lightEdit ? (
<LightEditFrame />
) : (
<Welcome />
)}
</div>
{openingPath && <ProjectOpening path={openingPath} />}
<OverlayHost />
<WindowResizeHandles />
</div>