You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/09/06 04:03:17 UTC
(maka) 01/01: fix(desktop): hug the right edge and ease the workbar column open and shut
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch fix/workbar-right-gutter-and-collapse-motion
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 50262532c6b551229a161d6e1794f19547b7e7c8
Author: AstroHan <le...@outlook.com>
AuthorDate: Sun Sep 6 11:59:28 2026 +0800
fix(desktop): hug the right edge and ease the workbar column open and shut
The right workbar's collapse toggle sat 24px in from the plate's edge on
macOS, where the platform draws nothing on the right: the titlebar strip and
the workbar's own bar each spelled out `--space-6 + overlay-right-width`.
Fold that into one `--maka-titlebar-gutter-right` token with the bar's own
`--space-2` inset, so the toggle keeps the same x in both bands and only
Windows caption buttons push it inward.
The column also snapped between `display: none` and its width, unlike the
sidebar. The frame and the overlay panel that share its grid area now keep
their width and margin as the animated properties, with `visibility` gated
by the same duration, and only the bottom placement still uses
`display: none`. Their children hold the open width during the transition so
the content slides past the clip edge instead of re-wrapping on every
frame. The panels stay mounted while hidden; a CDP trace of the 280ms ease
costs ~95ms of main thread over 33 frames with no drops at 120Hz.
`min-width`/`max-width` on the column go: `workbar-layout.ts` already clamps
the value, and a floor would stop the collapse short of 0. The E2E restore
assertion polls for the toggle's resting x, since it now rides in with the
column, and the new story asserts the gutter and samples the widths through
both directions of the motion.
Generated-by: Claude Code
---
apps/desktop/e2e/session-workbar.spec.ts | 6 +-
.../features/workbar/ui/workbar-surface.tsx | 11 +--
apps/desktop/src/renderer/maka-tokens.css | 6 ++
apps/desktop/src/renderer/styles/shell-layout.css | 10 ++-
apps/desktop/src/renderer/styles/workbar/shell.css | 56 ++++++++++-----
apps/desktop/stories/session-workbar.stories.tsx | 84 ++++++++++++++++++++--
6 files changed, 140 insertions(+), 33 deletions(-)
diff --git a/apps/desktop/e2e/session-workbar.spec.ts b/apps/desktop/e2e/session-workbar.spec.ts
index 22a6ae701d..d96621a23e 100644
--- a/apps/desktop/e2e/session-workbar.spec.ts
+++ b/apps/desktop/e2e/session-workbar.spec.ts
@@ -350,10 +350,14 @@ test('titlebar workbar action restores an existing tool instead of the picker',
name: '收起任务工作栏',
});
await expect(restoredCollapseButton).toBeVisible();
+ // The column eases open, so the toggle rides in from the right before it
+ // comes to rest where the titlebar's affordance stood.
+ await expect
+ .poll(async () => (await restoredCollapseButton.boundingBox())?.x)
+ .toBeCloseTo(safeAreaToggleBox!.x, 0);
const restoredToggleBox = await restoredCollapseButton.boundingBox();
expect(restoredToggleBox).not.toBeNull();
expect(Math.abs(restoredToggleBox!.y - safeAreaToggleBox!.y)).toBeLessThanOrEqual(1);
- expect(Math.abs(restoredToggleBox!.x - safeAreaToggleBox!.x)).toBeLessThanOrEqual(1);
});
test('Git changes re-read the workspace after the app regains focus', async ({
diff --git a/apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx b/apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx
index e0077ba8ba..5dce1cfe3f 100644
--- a/apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx
+++ b/apps/desktop/src/renderer/features/workbar/ui/workbar-surface.tsx
@@ -102,6 +102,7 @@ function WorkbarPanelLoading(props: { label: string }) {
function WorkbarPanel(props: {
id?: string;
active: boolean;
+ collapsed?: boolean;
placement: SessionWorkbarPlacement;
overlay?: boolean;
className?: string;
@@ -115,6 +116,7 @@ function WorkbarPanel(props: {
hidden={!props.active}
data-placement={props.placement}
data-overlay={props.overlay || undefined}
+ data-collapsed={props.collapsed || undefined}
className={
props.className
? `maka-session-workbar-panel ${props.className}`
@@ -465,7 +467,7 @@ export function WorkbarSurface(props: {
}
/>
</div>
- <WorkbarPanel active={visible && showingLauncher} placement={placement}>
+ <WorkbarPanel active={showingLauncher} placement={placement}>
<WorkbarLauncher
onOpen={(kind) => props.onRequestOpenTab(placement, kind)}
sideChatAvailable={props.sourceSession !== undefined}
@@ -480,8 +482,8 @@ export function WorkbarSurface(props: {
const showingLauncher = panel.launcherOpen || !activeTab;
const panelVisible =
placement === 'right' ? !props.rightCollapsed : props.bottomOpen;
- const active =
- panelVisible && !showingLauncher && activeTab?.id === tab.id;
+ const selected = !showingLauncher && activeTab?.id === tab.id;
+ const active = panelVisible && selected;
let content: ReactNode = null;
if (tab.kind === 'review') {
content = (
@@ -567,7 +569,8 @@ export function WorkbarSurface(props: {
<WorkbarPanel
key={tab.id}
id={`maka-workbar-panel-${tab.id}`}
- active={active}
+ active={selected}
+ collapsed={props.hidden || !panelVisible}
placement={placement}
overlay
className={
diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css
index 1ee3ad813d..d852328bbc 100644
--- a/apps/desktop/src/renderer/maka-tokens.css
+++ b/apps/desktop/src/renderer/maka-tokens.css
@@ -881,6 +881,12 @@
everything already standing between it and that edge. Two literals would be
two chances to change one of them. */
--maka-titlebar-gutter-left: max(var(--space-6), var(--maka-titlebar-area-x));
+ /* Windows caption buttons report their width here; macOS and Linux report 0.
+ Shared by the titlebar strip and the workbar's bar, which hand one collapse
+ toggle back and forth and hold it to the same x. */
+ --maka-titlebar-gutter-right: calc(
+ var(--space-2) + var(--maka-titlebar-overlay-right-width)
+ );
--maka-titlebar-gap: var(--space-2);
/* What the strip keeps for itself. This window has no OS title bar, so the
strip IS the drag handle, and the session breadcrumb is a grid item that
diff --git a/apps/desktop/src/renderer/styles/shell-layout.css b/apps/desktop/src/renderer/styles/shell-layout.css
index ce6ea21dd8..6b733a8c2b 100644
--- a/apps/desktop/src/renderer/styles/shell-layout.css
+++ b/apps/desktop/src/renderer/styles/shell-layout.css
@@ -267,13 +267,11 @@
--background right). Windows titleBarOverlay still samples --background
for the OS caption strip on the content side. */
background: transparent;
- /* One gutter rule per side: our own `--space-6` of breathing room, or the
- platform's native-control safe area when that is wider. Nothing here knows
- which platform it is on or how wide any control is — `max()` picks whichever
- the OS reports (macOS: traffic lights on the left; Windows: caption buttons
- on the right; Linux: neither, so the design floor stands). */
+ /* One gutter rule per side, both resolved in maka-tokens.css from what the
+ OS reports (macOS: traffic lights on the left; Windows: caption buttons on
+ the right; Linux: neither, so the design floors stand). */
padding-left: var(--maka-titlebar-gutter-left);
- padding-right: calc(var(--space-6) + var(--maka-titlebar-overlay-right-width));
+ padding-right: var(--maka-titlebar-gutter-right);
-webkit-app-region: drag;
/* Three columns, not a flex row, because the middle one has to line up with
diff --git a/apps/desktop/src/renderer/styles/workbar/shell.css b/apps/desktop/src/renderer/styles/workbar/shell.css
index f62b09e407..8ea90ca87b 100644
--- a/apps/desktop/src/renderer/styles/workbar/shell.css
+++ b/apps/desktop/src/renderer/styles/workbar/shell.css
@@ -63,12 +63,43 @@
box-sizing: border-box;
}
-.maka-session-workbar[data-placement="right"] {
+/* The frame and the overlay panel share the grid area, so they carry the same
+ width and margin and animate to the same track width on every frame. The
+ children keep the open width while the box shrinks, so the content slides
+ past the clip edge instead of re-wrapping. No `min-width` here: it would
+ stop the collapse short, and `workbar-layout.ts` already clamps the value. */
+.maka-session-workbar[data-placement="right"],
+.maka-session-workbar-panel[data-overlay][data-placement="right"] {
grid-area: right;
width: var(--maka-session-workbar-width, 480px);
- min-width: 320px;
- max-width: 600px;
margin-left: var(--agents-content-area-gap);
+ transition:
+ width var(--duration-large) var(--ease-out-strong),
+ margin-left var(--duration-large) var(--ease-out-strong),
+ visibility 0s;
+}
+
+.maka-session-workbar[data-placement="right"] > *,
+.maka-session-workbar-panel[data-overlay][data-placement="right"] > * {
+ width: var(--maka-session-workbar-width, 480px);
+}
+
+.maka-session-workbar[data-placement="right"][data-collapsed],
+.maka-session-workbar-panel[data-overlay][data-placement="right"][data-collapsed] {
+ width: 0;
+ margin-left: 0;
+ visibility: hidden;
+ transition:
+ width var(--duration-large) var(--ease-out-strong),
+ margin-left var(--duration-large) var(--ease-out-strong),
+ visibility 0s var(--duration-large);
+}
+
+/* A pointer drag feeds the width back on every move; an ease would trail it. */
+.maka-detail-with-artifacts:has([data-resizing]) .maka-session-workbar[data-placement="right"],
+.maka-detail-with-artifacts:has([data-resizing])
+ .maka-session-workbar-panel[data-overlay][data-placement="right"] {
+ transition: none;
}
.maka-session-workbar[data-placement="bottom"] {
@@ -81,7 +112,7 @@
grid-template-rows: auto minmax(0, 1fr);
}
-.maka-session-workbar[data-collapsed] {
+.maka-session-workbar[data-placement="bottom"][data-collapsed] {
display: none;
}
@@ -103,11 +134,6 @@
}
.maka-session-workbar-panel[data-overlay][data-placement="right"] {
- grid-area: right;
- width: var(--maka-session-workbar-width, 480px);
- min-width: 320px;
- max-width: 600px;
- margin-left: var(--agents-content-area-gap);
padding-top: var(--maka-plate-titlebar-clearance);
}
@@ -168,14 +194,10 @@
--maka-workbar-rail-gap: calc(
(var(--maka-plate-titlebar-clearance) - var(--size-element-sm)) / 2
);
- /* The right pad is the window titlebar strip's own gutter, not this bar's:
- the collapse toggle is one control that moves between the two bands, and
- `session-workbar.spec.ts` holds it to the same x in both. Narrowing this to
- the bar's own gutter would slide it 16px on every collapse. On top of it,
- the caption buttons where the platform draws them on the right (Windows);
- macOS puts them on the left, over the sidebar, and reports 0 here. */
- padding-inline: var(--space-2)
- calc(var(--space-6) + var(--maka-titlebar-overlay-right-width));
+ /* The right pad is the window titlebar strip's gutter: the collapse toggle
+ is one control that moves between the two bands, and
+ `session-workbar.spec.ts` holds it to the same x in both. */
+ padding-inline: var(--space-2) var(--maka-titlebar-gutter-right);
-webkit-app-region: drag;
}
diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx
index 168856188c..dbc9ef19ec 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,
@@ -928,7 +928,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;
@@ -986,13 +992,21 @@ function Workbar(props: {
...(props.width ? { '--maka-session-workbar-width': `${props.width}px` } : {}),
} as CSSProperties}
>
- <div className="mainColumn" />
+ <div className="mainColumn">
+ {props.collapsible && (
+ <WorkbarTitlebarActions
+ available
+ collapsed={collapsed}
+ onToggle={() => setCollapsed(false)}
+ />
+ )}
+ </div>
<WorkbarSurface
sessionId={SESSION_ID}
hidden={false}
- onDismissPanel={noop}
+ onDismissPanel={props.collapsible ? () => setCollapsed(true) : noop}
panelsState={createSessionWorkbarPanelsState(tabsState)}
- rightCollapsed={false}
+ rightCollapsed={collapsed}
bottomOpen={false}
onActivateTab={noop}
onCloseTab={noop}
@@ -1055,6 +1069,66 @@ export const SeveralFacesAtColumnFloor: Story = {
),
};
+// Real path: 变更 open → collapse from the bar → restore from the titlebar. The
+// column eases shut and open the way the sidebar does, with the open face
+// riding along instead of vanishing on the first frame, and the collapse
+// toggle sits one `--space-2` in from the plate's edge — the titlebar's
+// gutter on a platform that draws nothing there.
+export const CollapseAndRestore: Story = {
+ decorators: [bridge()],
+ render: () => <Workbar tab="review" collapsible />,
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const frame = canvasElement.querySelector<HTMLElement>(
+ '.maka-session-workbar[data-placement="right"]',
+ )!;
+ const panel = canvasElement.querySelector<HTMLElement>(
+ '.maka-session-workbar-panel[data-overlay][data-placement="right"]',
+ )!;
+ const sampleWidths = () =>
+ new Promise<{ frame: number[]; panel: number[] }>((resolve) => {
+ const out = { frame: [] as number[], panel: [] as number[] };
+ const start = performance.now();
+ const tick = () => {
+ out.frame.push(Math.round(frame.getBoundingClientRect().width));
+ out.panel.push(Math.round(panel.getBoundingClientRect().width));
+ if (performance.now() - start < 500) requestAnimationFrame(tick);
+ else resolve(out);
+ };
+ requestAnimationFrame(tick);
+ });
+ const distinct = (values: number[]) => new Set(values).size;
+ const monotonic = (values: number[], direction: 'down' | 'up') =>
+ values.every((value, index) =>
+ index === 0 || (direction === 'down' ? value <= values[index - 1] : value >= values[index - 1]),
+ );
+
+ const collapse = canvas.getByRole('button', { name: '收起任务工作栏' });
+ await canvas.findByRole('region', { name: 'Git 变更' });
+ const open = frame.getBoundingClientRect().width;
+ expect(frame.getBoundingClientRect().right - collapse.getBoundingClientRect().right).toBe(8);
+
+ let sampling = sampleWidths();
+ await userEvent.click(collapse);
+ let widths = await sampling;
+ expect(widths.frame.at(-1)).toBe(0);
+ expect(distinct(widths.frame)).toBeGreaterThanOrEqual(6);
+ expect(monotonic(widths.frame, 'down')).toBe(true);
+ expect(widths.panel).toEqual(widths.frame);
+ await expect(panel).not.toBeVisible();
+
+ const restore = canvas.getByRole('button', { name: '展开任务工作栏' });
+ sampling = sampleWidths();
+ await userEvent.click(restore);
+ widths = await sampling;
+ expect(widths.frame.at(-1)).toBe(open);
+ expect(distinct(widths.frame)).toBeGreaterThanOrEqual(6);
+ expect(monotonic(widths.frame, 'up')).toBe(true);
+ expect(widths.panel).toEqual(widths.frame);
+ await expect(canvas.getByRole('region', { name: 'Git 变更' })).toBeVisible();
+ },
+};
+
// 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 = {
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org