>, toggleX: number) => {
+ expect(sampled.eased).toBe(true);
+ expect(sampled.panel).toEqual(sampled.frame);
+ expect(new Set(sampled.faceRight)).toEqual(new Set([0]));
+ expect(new Set(sampled.toggleX)).toEqual(new Set([Math.round(toggleX)]));
+ };
expect(canvas.queryByRole('toolbar', { name: '工作区辅助操作' })).toBeNull();
expect(pickerIsShowing()).toBe(false);
@@ -3082,46 +3129,57 @@ export const WorkbarCollapseKeepsOneToggleInPlace: Story = {
faceBox.y + faceBox.height / 2 - (openToggleBox.y + openToggleBox.height / 2),
),
).toBeLessThanOrEqual(1);
+ expect(frame.getBoundingClientRect().right - openToggleBox.right).toBe(8);
// Windows draws its caption buttons over the right of the strip and reports
// their width here; macOS reports 0. The bar has to give that width back.
+ // The root is where `maka-tokens.css` reads it into the gutter, so the
+ // override goes there, the way `env()` would report it.
const captionWidth = 80;
- canvasElement.style.setProperty(
+ document.documentElement.style.setProperty(
'--maka-titlebar-overlay-right-width',
`${captionWidth}px`,
);
- await waitFor(() => {
- expect(collapse.getBoundingClientRect().x).toBeCloseTo(
- openToggleBox.x - captionWidth,
- 0,
- );
- });
- const parked = collapse.getBoundingClientRect();
-
- // [+] is a menu over the panel, not a swap to the launcher: the face you
- // are reading stays on screen while you pick another one.
- await userEvent.click(bar.getByRole('button', { name: '打开或关闭工作栏的面' }));
- const menu = await within(document.body).findByRole('menu');
- await userEvent.keyboard('{Escape}');
- await waitFor(() => expect(menu).not.toBeVisible());
- expect(pickerIsShowing()).toBe(false);
-
- await userEvent.click(collapse);
- const restore = await canvas.findByRole('button', { name: '展开任务工作栏' });
- await waitFor(() => expect(frame).not.toBeVisible());
- expect(facePanel).not.toBeVisible();
- const restoreBox = restore.getBoundingClientRect();
- expect(Math.abs(restoreBox.x - parked.x)).toBeLessThanOrEqual(1);
- expect(Math.abs(restoreBox.y - parked.y)).toBeLessThanOrEqual(1);
-
- await userEvent.click(restore);
- await waitFor(() => expect(frame).toBeVisible());
- expect(facePanel).toBeVisible();
- expect(pickerIsShowing()).toBe(false);
- const restoredToggleBox = bar
- .getByRole('button', { name: '收起任务工作栏' })
- .getBoundingClientRect();
- expect(Math.abs(restoredToggleBox.x - parked.x)).toBeLessThanOrEqual(1);
- expect(Math.abs(restoredToggleBox.y - parked.y)).toBeLessThanOrEqual(1);
+ try {
+ await waitFor(() => {
+ expect(collapse.getBoundingClientRect().x).toBeCloseTo(
+ openToggleBox.x - captionWidth,
+ 0,
+ );
+ });
+ const parked = collapse.getBoundingClientRect();
+
+ // [+] is a menu over the panel, not a swap to the launcher: the face you
+ // are reading stays on screen while you pick another one.
+ await userEvent.click(bar.getByRole('button', { name: '打开或关闭工作栏的面' }));
+ const menu = await within(document.body).findByRole('menu');
+ await userEvent.keyboard('{Escape}');
+ await waitFor(() => expect(menu).not.toBeVisible());
+ expect(pickerIsShowing()).toBe(false);
+
+ let sampling = sample();
+ await userEvent.click(collapse);
+ const restore = await canvas.findByRole('button', { name: '展开任务工作栏' });
+ eased(await sampling, parked.x);
+ await waitFor(() => expect(frame).not.toBeVisible());
+ expect(facePanel).not.toBeVisible();
+ const restoreBox = restore.getBoundingClientRect();
+ expect(Math.abs(restoreBox.x - parked.x)).toBeLessThanOrEqual(1);
+ expect(Math.abs(restoreBox.y - parked.y)).toBeLessThanOrEqual(1);
+
+ sampling = sample();
+ await userEvent.click(restore);
+ eased(await sampling, parked.x);
+ await waitFor(() => expect(frame).toBeVisible());
+ expect(facePanel).toBeVisible();
+ expect(pickerIsShowing()).toBe(false);
+ const restoredToggleBox = bar
+ .getByRole('button', { name: '收起任务工作栏' })
+ .getBoundingClientRect();
+ expect(Math.abs(restoredToggleBox.x - parked.x)).toBeLessThanOrEqual(1);
+ expect(Math.abs(restoredToggleBox.y - parked.y)).toBeLessThanOrEqual(1);
+ } finally {
+ document.documentElement.style.removeProperty('--maka-titlebar-overlay-right-width');
+ }
},
};
diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx
index 4c25fc3043..6c0889d8db 100644
--- a/apps/desktop/stories/session-workbar.stories.tsx
+++ b/apps/desktop/stories/session-workbar.stories.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import type { CSSProperties } from 'react';
+import { useState, type CSSProperties } from 'react';
import type { Decorator, Meta, StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import type { ArtifactRecord } from '@maka/core/artifacts';
@@ -27,7 +27,7 @@ import type { SessionSummary } from '@maka/core/session';
import type { SessionTrace } from '@maka/core/session-trace';
import type { ContextDiagnosticsResult } from '@maka/runtime-host/protocol';
import { ToastProvider } from '@maka/ui';
-import { WorkbarServicesProvider } from '../src/renderer/features/workbar';
+import { WorkbarServicesProvider, WorkbarTitlebarActions } from '../src/renderer/features/workbar';
import { WorkbarSurface } from '../src/renderer/features/workbar/stories';
import {
createFakeWorkbarServices,
@@ -927,7 +927,13 @@ function Workbar(props: {
sourceSession?: SessionSummary;
/** Overrides the restored column width, the way the resize handle does. */
width?: number;
+ /**
+ * Lets the column's own collapse toggle and the titlebar's restore
+ * affordance drive `rightCollapsed`, the way the app's reducer does.
+ */
+ collapsible?: boolean;
}) {
+ const [collapsed, setCollapsed] = useState(false);
const emptyTabsState = createSessionWorkbarTabsState();
let tab: SessionWorkbarTab | undefined;
let quotes: QuoteCompanionPanelState[] | undefined;
@@ -985,13 +991,21 @@ function Workbar(props: {
...(props.width ? { '--maka-session-workbar-width': `${props.width}px` } : {}),
} as CSSProperties}
>
-
+
+ {props.collapsible && (
+ setCollapsed(false)}
+ />
+ )}
+
setCollapsed(true) : noop}
panelsState={createSessionWorkbarPanelsState(tabsState)}
- rightCollapsed={false}
+ rightCollapsed={collapsed}
bottomOpen={false}
onActivateTab={noop}
onCloseTab={noop}
@@ -1076,6 +1090,37 @@ export const SeveralFacesAtColumnFloor: Story = {
),
};
+// Below 991px the column stacks under the conversation at full width. The
+// wide-window ease (app-shell.stories.tsx holds that contract) must not reach
+// it: the face spans the row, and collapsing removes the row instead of
+// leaving an empty band. The smoke lane sizes stories named `narrow` to 720px.
+export const CollapseNarrowStack: Story = {
+ parameters: { viewport: { options: STACKED_WINDOW_VIEWPORT } },
+ globals: { viewport: { value: 'makaStackedWindow', isRotated: false } },
+ decorators: [bridge()],
+ render: () => ,
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const frame = canvasElement.querySelector(
+ '.maka-session-workbar[data-placement="right"]',
+ )!;
+ const panel = canvasElement.querySelector(
+ '.maka-session-workbar-panel[data-overlay][data-placement="right"]',
+ )!;
+ const toolbar = frame.querySelector('.maka-session-workbar-toolbar')!;
+ await canvas.findByRole('region', { name: 'Git 变更' });
+ expect(window.innerWidth).toBeLessThanOrEqual(990);
+ expect(toolbar.getBoundingClientRect().width).toBe(frame.getBoundingClientRect().width);
+ expect(panel.firstElementChild!.getBoundingClientRect().width).toBe(
+ panel.getBoundingClientRect().width,
+ );
+
+ await userEvent.click(canvas.getByRole('button', { name: '收起任务工作栏' }));
+ await waitFor(() => expect(getComputedStyle(frame).display).toBe('none'));
+ expect(getComputedStyle(panel).display).toBe('none');
+ },
+};
+
// Real path: 任务工作栏 → 变更 on a session whose branch matches its base. The
// panel's own empty state (icon + help), not a spinner and not an error.
export const ChangesEmpty: Story = {