mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-06 03:37:50 +00:00
fix: retire bottom Workbench docking (#1390)
This commit is contained in:
parent
c646932c85
commit
089e45a63f
12 changed files with 138 additions and 314 deletions
|
|
@ -171,9 +171,9 @@ Spawn autonomous coding agents on tasks when you choose to connect an agent runn
|
|||
|
||||

|
||||
|
||||
Desktop Board Chat and Squad Chat open in a bounded right-side Workbench dock by
|
||||
default. Switch to Bottom when vertical space is preferable; both orientations
|
||||
keep the board, header, close control, and keyboard recovery paths reachable.
|
||||
Desktop Board Chat and Squad Chat open in one bounded, resizable right-side
|
||||
Workbench dock. The single orientation keeps the board, header, close control,
|
||||
and keyboard recovery paths reachable at every supported window size.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ The Kanban board is the central interface — a drag-and-drop workspace that ref
|
|||
- **Desktop shell controls** — Native-app-style toolbar with workspace selection, health state, view toggles, and bounded left/right/chat dock controls shared by the web and macOS app shells
|
||||
- **Native version identity** — The macOS application menu opens an offline About panel and copies a redacted support string from the same authoritative Electron version, embedded release commit, release channel, OS, and architecture record exposed by the desktop bridge
|
||||
- **Mobile shell controls** — Compact navigation uses bounded labels and full accessible names; Board Chat stays fixed above the bottom navigation and device safe area
|
||||
- **Resizable Workbench** — Board Chat and Squad Chat open in a right dock by default, can switch to Bottom without losing the active conversation, and clamp their width or height to keep the application shell recoverable
|
||||
- **Resizable Workbench** — Board Chat and Squad Chat open in one bounded right-side dock, preserve the active conversation when switching channels, and clamp their width to keep the application shell recoverable
|
||||
- **Bulk operations** — Select multiple tasks to move, archive, or delete in batch; select-all toggle
|
||||
- **Keyboard shortcuts** — Navigate tasks (j/k, arrows), open (Enter), close (Esc), create (c), move to column (1-4), help (?)
|
||||
- **Loading skeleton** — Shimmer placeholders while the board loads
|
||||
|
|
@ -636,7 +636,7 @@ Real-time agent-to-agent communication channel for multi-agent collaboration. Sh
|
|||
|  |  |
|
||||
|
||||
- **WebSocket-powered chat** — Messages broadcast in real time to all connected clients
|
||||
- **Resizable Workbench dock** — Board Chat and Squad Chat share one dock that defaults Right, optionally moves to Bottom, isolates chat scrolling, and keeps Close, Escape, Back, and Reset Layout recovery available
|
||||
- **Resizable Workbench dock** — Board Chat and Squad Chat share one bounded right-side dock that isolates chat scrolling and keeps Close, Escape, Back, and Reset Layout recovery available
|
||||
- **Local shared log** — Squad Chat stores and streams messages; it does not wake or reply through an external agent unless a webhook, OpenClaw Direct path, or orchestrator is configured
|
||||
- **Threaded coordination** — Reply-to links render compact threads for long multi-agent runs
|
||||
- **Unread and mentions** — Per-actor unread state persists across refreshes, and mentions create local notifications linked back to messages
|
||||
|
|
|
|||
|
|
@ -1755,8 +1755,8 @@ curl -X POST .../tasks/<id>/comments \
|
|||
<div>
|
||||
<h3>Bounded Chat Workbench</h3>
|
||||
<p>
|
||||
Board Chat and Squad Chat default to a right dock, switch to Bottom without
|
||||
remounting, clamp to the viewport, and always preserve visible recovery controls.
|
||||
Board Chat and Squad Chat share one bounded right-side dock, switch channels without
|
||||
remounting, clamp to the viewport, and preserve visible recovery controls.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ test.describe('Desktop board shell containment', () => {
|
|||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem('veritas-kanban-theme', 'dark');
|
||||
window.localStorage.setItem('veritas.desktop.rightRailOpen', 'true');
|
||||
window.localStorage.setItem('veritas.workbench.dockPosition', 'right');
|
||||
window.localStorage.setItem('veritas.workbench.dockPosition', 'bottom');
|
||||
window.localStorage.setItem('veritas.workbench.bottomPanelHeight', '640');
|
||||
Object.defineProperty(window, 'veritasDesktop', {
|
||||
configurable: true,
|
||||
value: {
|
||||
|
|
@ -46,12 +47,24 @@ test.describe('Desktop board shell containment', () => {
|
|||
await page.goto('/');
|
||||
await expect(page.locator('html')).toHaveAttribute('data-client', 'desktop');
|
||||
await expect(page.getByLabel('Board right sidebar')).toBeVisible();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => window.localStorage.getItem('veritas.workbench.dockPosition'))
|
||||
)
|
||||
.toBeNull();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => window.localStorage.getItem('veritas.workbench.bottomPanelHeight'))
|
||||
)
|
||||
.toBeNull();
|
||||
await Promise.all(
|
||||
taskIds.map((taskId) => expect(page.locator(`[data-task-id="${taskId}"]`)).toBeAttached())
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Open chat dock' }).click();
|
||||
await expect(page.getByRole('region', { name: 'Workbench right dock' })).toBeVisible();
|
||||
const workbench = page.getByRole('region', { name: 'Workbench right dock' });
|
||||
await expect(workbench).toBeVisible();
|
||||
await expect(workbench.getByRole('radiogroup', { name: 'Dock position' })).toHaveCount(0);
|
||||
|
||||
const readViewport = () =>
|
||||
page.evaluate(() => ({
|
||||
|
|
@ -68,6 +81,21 @@ test.describe('Desktop board shell containment', () => {
|
|||
};
|
||||
expect(await readViewport()).toEqual(expectedViewport);
|
||||
|
||||
const resizer = page.getByRole('button', { name: 'Resize right dock' });
|
||||
const widthBeforeResize = await workbench.evaluate(
|
||||
(element) => element.getBoundingClientRect().width
|
||||
);
|
||||
await resizer.focus();
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect
|
||||
.poll(() => workbench.evaluate((element) => element.getBoundingClientRect().width))
|
||||
.toBeGreaterThan(widthBeforeResize);
|
||||
expect(await readViewport()).toEqual(expectedViewport);
|
||||
|
||||
await page.getByRole('button', { name: 'Switch to Squad Chat' }).click();
|
||||
await expect(page.getByRole('region', { name: 'Squad Chat' })).toBeVisible();
|
||||
expect(await readViewport()).toEqual(expectedViewport);
|
||||
|
||||
await page.getByRole('button', { name: 'Close right dock' }).click();
|
||||
await expect(page.getByRole('region', { name: 'Workbench right dock' })).toHaveCount(0);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,19 @@ async function useDarkTheme(page: Page) {
|
|||
});
|
||||
}
|
||||
|
||||
async function useDesktopBridge(page: Page) {
|
||||
await page.evaluate(() => {
|
||||
Object.defineProperty(window, 'veritasDesktop', {
|
||||
configurable: true,
|
||||
value: {
|
||||
onMenuCommand: () => () => undefined,
|
||||
toggleWindowMaximize: async () => ({ maximized: false }),
|
||||
},
|
||||
});
|
||||
});
|
||||
await page.reload();
|
||||
}
|
||||
|
||||
test('captures public-safe 6.1.6 desktop and mobile documentation media', async ({ page }) => {
|
||||
test.setTimeout(180_000);
|
||||
await mkdir(assetsDir, { recursive: true });
|
||||
|
|
@ -63,6 +76,7 @@ test('captures public-safe 6.1.6 desktop and mobile documentation media', async
|
|||
try {
|
||||
await page.setViewportSize(desktopViewport);
|
||||
await page.goto('/');
|
||||
await useDesktopBridge(page);
|
||||
await expect(page.getByRole('region', { name: 'To Do' })).toBeVisible({ timeout: 15_000 });
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Prepare the release candidate' })
|
||||
|
|
@ -102,15 +116,19 @@ test('captures public-safe 6.1.6 desktop and mobile documentation media', async
|
|||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.getByRole('button', { name: 'Open Board Chat' }).click();
|
||||
await expect(page.getByRole('region', { name: 'Workbench bottom dock' })).toBeVisible();
|
||||
await expect(page.getByRole('region', { name: 'Workbench right dock' })).toBeVisible();
|
||||
await expect(page.locator('section[aria-label="Board Chat"]')).toBeVisible();
|
||||
await capture(page, 'workbench-panel.png');
|
||||
await page.getByText('Squad Chat', { exact: true }).last().click();
|
||||
await expect(page.getByRole('region', { name: 'Squad Chat' })).toBeVisible();
|
||||
await capture(page, 'squad-chat.png');
|
||||
await page.getByRole('button', { name: 'Close bottom dock' }).click();
|
||||
await page.getByRole('button', { name: 'Close right dock' }).click();
|
||||
|
||||
await page.setViewportSize(mobileViewport);
|
||||
await page.evaluate(() => {
|
||||
delete (window as Window & { veritasDesktop?: unknown }).veritasDesktop;
|
||||
delete document.documentElement.dataset.client;
|
||||
});
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Mobile board' }).click();
|
||||
await expect(page.getByRole('region', { name: 'To Do' })).toBeVisible({ timeout: 15_000 });
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ function DesktopAwareAppShell({
|
|||
showDesktopOnboarding: boolean;
|
||||
setShowDesktopOnboarding: (open: boolean) => void;
|
||||
}) {
|
||||
const { isDesktopClient, bottomPanel, dockPosition } = useDesktopShell();
|
||||
const { isDesktopClient, bottomPanel } = useDesktopShell();
|
||||
|
||||
return (
|
||||
<Box className="desktop-app-shell min-h-screen bg-background">
|
||||
|
|
@ -132,10 +132,7 @@ function DesktopAwareAppShell({
|
|||
<Suspense fallback={null}>
|
||||
<PwaStatusBanner sessionExpiry={authStatus?.sessionExpiry} onRefreshAuth={refreshStatus} />
|
||||
</Suspense>
|
||||
<div
|
||||
className="desktop-workbench-container"
|
||||
data-dock-position={bottomPanel ? dockPosition : undefined}
|
||||
>
|
||||
<div className="desktop-workbench-container">
|
||||
<div className="desktop-workbench">
|
||||
<DesktopLeftSidebar />
|
||||
<Box
|
||||
|
|
|
|||
|
|
@ -16,18 +16,10 @@ function ShellProbe() {
|
|||
<output aria-label="left rail">{String(shell.leftRailOpen)}</output>
|
||||
<output aria-label="right rail">{String(shell.rightRailOpen)}</output>
|
||||
<output aria-label="bottom panel">{shell.bottomPanel ?? 'closed'}</output>
|
||||
<output aria-label="dock position">{shell.dockPosition}</output>
|
||||
<output aria-label="bottom panel height">{shell.bottomPanelHeight}</output>
|
||||
<output aria-label="right panel width">{shell.rightPanelWidth}</output>
|
||||
<button type="button" onClick={() => shell.openBottomPanel('board-chat')}>
|
||||
Open board chat
|
||||
</button>
|
||||
<button type="button" onClick={() => shell.setDockPosition('bottom')}>
|
||||
Dock bottom
|
||||
</button>
|
||||
<button type="button" onClick={() => shell.setDockPosition('right')}>
|
||||
Dock right
|
||||
</button>
|
||||
<button type="button" onClick={() => shell.setLeftRailOpen(false)}>
|
||||
Close left rail
|
||||
</button>
|
||||
|
|
@ -78,11 +70,11 @@ describe('desktop shell recovery', () => {
|
|||
menuListener = undefined;
|
||||
});
|
||||
|
||||
it('defaults invalid and legacy layout state to a bounded right dock', () => {
|
||||
it('retires legacy bottom-dock state and defaults to a bounded right dock', () => {
|
||||
window.localStorage.setItem('veritas.desktop.bottomPanel', 'board-chat');
|
||||
window.localStorage.setItem('veritas.workbench.bottomPanelHeight', '9999');
|
||||
window.localStorage.setItem('veritas.workbench.rightPanelWidth', '-20');
|
||||
window.localStorage.setItem('veritas.workbench.dockPosition', 'full-window');
|
||||
window.localStorage.setItem('veritas.workbench.dockPosition', 'bottom');
|
||||
|
||||
render(
|
||||
<DesktopShellProvider>
|
||||
|
|
@ -91,37 +83,27 @@ describe('desktop shell recovery', () => {
|
|||
);
|
||||
|
||||
expect(screen.getByLabelText('bottom panel').textContent).toBe('closed');
|
||||
expect(screen.getByLabelText('dock position').textContent).toBe('right');
|
||||
expect(screen.getByLabelText('bottom panel height').textContent).toBe('280');
|
||||
expect(screen.getByLabelText('right panel width').textContent).toBe(
|
||||
String(DEFAULT_RIGHT_PANEL_WIDTH)
|
||||
);
|
||||
expect(window.localStorage.getItem('veritas.desktop.bottomPanel')).toBeNull();
|
||||
expect(window.localStorage.getItem('veritas.workbench.bottomPanelHeight')).toBeNull();
|
||||
expect(window.localStorage.getItem('veritas.workbench.dockPosition')).toBeNull();
|
||||
});
|
||||
|
||||
it('persists a valid dock position without closing the conversation', async () => {
|
||||
it('opens the desktop conversation without recreating retired dock preferences', async () => {
|
||||
const user = userEvent.setup();
|
||||
const view = render(
|
||||
render(
|
||||
<DesktopShellProvider>
|
||||
<ShellProbe />
|
||||
</DesktopShellProvider>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Open board chat' }));
|
||||
await user.click(screen.getByRole('button', { name: 'Dock bottom' }));
|
||||
|
||||
expect(screen.getByLabelText('bottom panel').textContent).toBe('board-chat');
|
||||
expect(screen.getByLabelText('dock position').textContent).toBe('bottom');
|
||||
expect(window.localStorage.getItem('veritas.workbench.dockPosition')).toBe('bottom');
|
||||
|
||||
view.unmount();
|
||||
render(
|
||||
<DesktopShellProvider>
|
||||
<ShellProbe />
|
||||
</DesktopShellProvider>
|
||||
);
|
||||
expect(screen.getByLabelText('dock position').textContent).toBe('bottom');
|
||||
expect(window.localStorage.getItem('veritas.workbench.bottomPanelHeight')).toBeNull();
|
||||
expect(window.localStorage.getItem('veritas.workbench.dockPosition')).toBeNull();
|
||||
});
|
||||
|
||||
it('closes transient chat layout with Escape or browser Back and restores focus', async () => {
|
||||
|
|
@ -164,8 +146,6 @@ describe('desktop shell recovery', () => {
|
|||
expect(screen.getByLabelText('left rail').textContent).toBe('true');
|
||||
expect(screen.getByLabelText('right rail').textContent).toBe('false');
|
||||
expect(screen.getByLabelText('bottom panel').textContent).toBe('closed');
|
||||
expect(screen.getByLabelText('dock position').textContent).toBe('right');
|
||||
expect(screen.getByLabelText('bottom panel height').textContent).toBe('280');
|
||||
expect(screen.getByLabelText('right panel width').textContent).toBe(
|
||||
String(DEFAULT_RIGHT_PANEL_WIDTH)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -308,43 +308,40 @@ describe('layout chrome Mantine migration', () => {
|
|||
expect(activeItem.className).not.toContain('bg-primary/15');
|
||||
});
|
||||
|
||||
it.each(['right', 'bottom'] as const)(
|
||||
'toggles and switches the header chat controls with the %s dock',
|
||||
async (dockPosition) => {
|
||||
window.localStorage.setItem('veritas.workbench.dockPosition', dockPosition);
|
||||
const user = userEvent.setup();
|
||||
renderDesktopHeaderChrome({ withBottomPanel: true });
|
||||
it('toggles and switches the header chat controls with the right dock', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderDesktopHeaderChrome({ withBottomPanel: true });
|
||||
|
||||
const openBoard = screen.getByRole('button', { name: 'Open Board Chat' });
|
||||
const openSquad = screen.getByRole('button', { name: 'Open Squad Chat' });
|
||||
expect(openBoard.getAttribute('aria-pressed')).toBe('false');
|
||||
expect(openSquad.getAttribute('aria-pressed')).toBe('false');
|
||||
const openBoard = screen.getByRole('button', { name: 'Open Board Chat' });
|
||||
const openSquad = screen.getByRole('button', { name: 'Open Squad Chat' });
|
||||
expect(openBoard.getAttribute('aria-pressed')).toBe('false');
|
||||
expect(openSquad.getAttribute('aria-pressed')).toBe('false');
|
||||
|
||||
await user.click(openBoard);
|
||||
const closeBoard = screen.getByRole('button', { name: 'Close Board Chat' });
|
||||
expect(closeBoard.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(screen.getByRole('button', { name: 'Switch to Squad Chat' })).toBeDefined();
|
||||
await user.click(openBoard);
|
||||
const closeBoard = screen.getByRole('button', { name: 'Close Board Chat' });
|
||||
expect(closeBoard.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(screen.getByRole('button', { name: 'Switch to Squad Chat' })).toBeDefined();
|
||||
|
||||
await user.click(closeBoard);
|
||||
expect(screen.getByRole('button', { name: 'Open Board Chat' })).toBeDefined();
|
||||
await user.click(closeBoard);
|
||||
expect(screen.getByRole('button', { name: 'Open Board Chat' })).toBeDefined();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Open Board Chat' }));
|
||||
await user.click(screen.getByRole('button', { name: 'Switch to Squad Chat' }));
|
||||
const closeSquad = screen.getByRole('button', { name: 'Close Squad Chat' });
|
||||
expect(closeSquad.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(screen.getByRole('button', { name: 'Switch to Board Chat' })).toBeDefined();
|
||||
await user.click(screen.getByRole('button', { name: 'Open Board Chat' }));
|
||||
await user.click(screen.getByRole('button', { name: 'Switch to Squad Chat' }));
|
||||
const closeSquad = screen.getByRole('button', { name: 'Close Squad Chat' });
|
||||
expect(closeSquad.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(screen.getByRole('button', { name: 'Switch to Board Chat' })).toBeDefined();
|
||||
|
||||
await user.click(closeSquad);
|
||||
await waitFor(() => {
|
||||
const board = screen.getByRole('button', { name: 'Open Board Chat' });
|
||||
const squad = screen.getByRole('button', { name: 'Open Squad Chat' });
|
||||
expect([board, squad]).toContain(document.activeElement);
|
||||
});
|
||||
await user.click(closeSquad);
|
||||
await waitFor(() => {
|
||||
const board = screen.getByRole('button', { name: 'Open Board Chat' });
|
||||
const squad = screen.getByRole('button', { name: 'Open Squad Chat' });
|
||||
expect([board, squad]).toContain(document.activeElement);
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Open Board Chat' }));
|
||||
const dock = screen.getByRole('region', { name: `Workbench ${dockPosition} dock` });
|
||||
await user.click(within(dock).getByRole('button', { name: `Close ${dockPosition} dock` }));
|
||||
expect(screen.getByRole('button', { name: 'Open Board Chat' })).toBeDefined();
|
||||
}
|
||||
);
|
||||
await user.click(screen.getByRole('button', { name: 'Open Board Chat' }));
|
||||
const dock = screen.getByRole('region', { name: 'Workbench right dock' });
|
||||
expect(within(dock).queryByRole('radiogroup', { name: 'Dock position' })).toBeNull();
|
||||
await user.click(within(dock).getByRole('button', { name: 'Close right dock' }));
|
||||
expect(screen.getByRole('button', { name: 'Open Board Chat' })).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,23 +8,13 @@ import {
|
|||
type WheelEvent,
|
||||
} from 'react';
|
||||
import { ActionIcon, Group, SegmentedControl, Text } from '@mantine/core';
|
||||
import {
|
||||
GripHorizontal,
|
||||
GripVertical,
|
||||
MessageSquare,
|
||||
PanelBottomClose,
|
||||
PanelRightClose,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import { GripVertical, MessageSquare, PanelRightClose, Users } from 'lucide-react';
|
||||
|
||||
import {
|
||||
MAX_BOTTOM_PANEL_HEIGHT,
|
||||
MAX_RIGHT_PANEL_WIDTH,
|
||||
MIN_BOTTOM_PANEL_HEIGHT,
|
||||
MIN_RIGHT_PANEL_WIDTH,
|
||||
useDesktopShell,
|
||||
type DesktopBottomPanel as DesktopBottomPanelId,
|
||||
type DesktopDockPosition,
|
||||
} from './DesktopShellContext';
|
||||
|
||||
const ChatPanel = lazy(() =>
|
||||
|
|
@ -44,54 +34,30 @@ const PANEL_OPTIONS = [
|
|||
{ label: 'Squad Chat', value: 'squad-chat' },
|
||||
] satisfies Array<{ label: string; value: DesktopBottomPanelId }>;
|
||||
|
||||
const DOCK_OPTIONS = [
|
||||
{ label: 'Right', value: 'right' },
|
||||
{ label: 'Bottom', value: 'bottom' },
|
||||
] satisfies Array<{ label: string; value: DesktopDockPosition }>;
|
||||
|
||||
export function DesktopBottomPanel() {
|
||||
const {
|
||||
isDesktopClient,
|
||||
bottomPanel,
|
||||
dockPosition,
|
||||
bottomPanelHeight,
|
||||
rightPanelWidth,
|
||||
setDockPosition,
|
||||
setBottomPanelHeight,
|
||||
setRightPanelWidth,
|
||||
openBottomPanel,
|
||||
closeBottomPanel,
|
||||
} = useDesktopShell();
|
||||
const { bottomPanel, rightPanelWidth, setRightPanelWidth, openBottomPanel, closeBottomPanel } =
|
||||
useDesktopShell();
|
||||
const dragStateRef = useRef<{ startPosition: number; startSize: number } | null>(null);
|
||||
|
||||
if (!bottomPanel) return null;
|
||||
|
||||
const resizeBy = (delta: number) => {
|
||||
if (dockPosition === 'right') {
|
||||
setRightPanelWidth(rightPanelWidth + delta);
|
||||
} else {
|
||||
setBottomPanelHeight(bottomPanelHeight + delta);
|
||||
}
|
||||
setRightPanelWidth(rightPanelWidth + delta);
|
||||
};
|
||||
|
||||
const handleResizePointerDown = (event: PointerEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
dragStateRef.current = {
|
||||
startPosition: dockPosition === 'right' ? event.clientX : event.clientY,
|
||||
startSize: dockPosition === 'right' ? rightPanelWidth : bottomPanelHeight,
|
||||
startPosition: event.clientX,
|
||||
startSize: rightPanelWidth,
|
||||
};
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
};
|
||||
|
||||
const handleResizePointerMove = (event: PointerEvent<HTMLButtonElement>) => {
|
||||
if (!dragStateRef.current) return;
|
||||
const currentPosition = dockPosition === 'right' ? event.clientX : event.clientY;
|
||||
const delta = dragStateRef.current.startPosition - currentPosition;
|
||||
if (dockPosition === 'right') {
|
||||
setRightPanelWidth(dragStateRef.current.startSize + delta);
|
||||
} else {
|
||||
setBottomPanelHeight(dragStateRef.current.startSize + delta);
|
||||
}
|
||||
const delta = dragStateRef.current.startPosition - event.clientX;
|
||||
setRightPanelWidth(dragStateRef.current.startSize + delta);
|
||||
};
|
||||
|
||||
const handleResizePointerEnd = (event: PointerEvent<HTMLButtonElement>) => {
|
||||
|
|
@ -102,28 +68,18 @@ export function DesktopBottomPanel() {
|
|||
};
|
||||
|
||||
const handleResizeKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||
const growKey = dockPosition === 'right' ? 'ArrowLeft' : 'ArrowUp';
|
||||
const shrinkKey = dockPosition === 'right' ? 'ArrowRight' : 'ArrowDown';
|
||||
if (event.key === growKey) {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
resizeBy(event.shiftKey ? 80 : 24);
|
||||
} else if (event.key === shrinkKey) {
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
resizeBy(event.shiftKey ? -80 : -24);
|
||||
} else if (event.key === 'Home') {
|
||||
event.preventDefault();
|
||||
if (dockPosition === 'right') {
|
||||
setRightPanelWidth(MIN_RIGHT_PANEL_WIDTH);
|
||||
} else {
|
||||
setBottomPanelHeight(MIN_BOTTOM_PANEL_HEIGHT);
|
||||
}
|
||||
setRightPanelWidth(MIN_RIGHT_PANEL_WIDTH);
|
||||
} else if (event.key === 'End') {
|
||||
event.preventDefault();
|
||||
if (dockPosition === 'right') {
|
||||
setRightPanelWidth(MAX_RIGHT_PANEL_WIDTH);
|
||||
} else {
|
||||
setBottomPanelHeight(MAX_BOTTOM_PANEL_HEIGHT);
|
||||
}
|
||||
setRightPanelWidth(MAX_RIGHT_PANEL_WIDTH);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -132,16 +88,13 @@ export function DesktopBottomPanel() {
|
|||
event.stopPropagation();
|
||||
};
|
||||
|
||||
const orientationLabel = dockPosition === 'right' ? 'right dock' : 'bottom dock';
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`workbench-chat-dock workbench-chat-dock--${dockPosition} bg-card`}
|
||||
aria-label={`Workbench ${orientationLabel}`}
|
||||
data-dock-position={dockPosition}
|
||||
className="workbench-chat-dock workbench-chat-dock--right bg-card"
|
||||
aria-label="Workbench right dock"
|
||||
data-dock-position="right"
|
||||
style={
|
||||
{
|
||||
'--workbench-bottom-panel-height': `${bottomPanelHeight}px`,
|
||||
'--workbench-right-panel-width': `${rightPanelWidth}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
|
|
@ -149,9 +102,9 @@ export function DesktopBottomPanel() {
|
|||
<button
|
||||
type="button"
|
||||
className="workbench-chat-dock-resizer desktop-no-drag"
|
||||
aria-label={`Resize ${orientationLabel}`}
|
||||
aria-orientation={dockPosition === 'right' ? 'vertical' : 'horizontal'}
|
||||
title={`Drag to resize ${orientationLabel}`}
|
||||
aria-label="Resize right dock"
|
||||
aria-orientation="vertical"
|
||||
title="Drag to resize right dock"
|
||||
onPointerDown={handleResizePointerDown}
|
||||
onPointerMove={handleResizePointerMove}
|
||||
onPointerUp={handleResizePointerEnd}
|
||||
|
|
@ -159,11 +112,7 @@ export function DesktopBottomPanel() {
|
|||
onKeyDown={handleResizeKeyDown}
|
||||
onWheel={handleResizeWheel}
|
||||
>
|
||||
{dockPosition === 'right' ? (
|
||||
<GripVertical className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
) : (
|
||||
<GripHorizontal className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
)}
|
||||
<GripVertical className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
<div className="desktop-no-drag shrink-0 space-y-2 border-b border-border px-3 py-2">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
|
|
@ -178,28 +127,15 @@ export function DesktopBottomPanel() {
|
|||
</Text>
|
||||
</Group>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
{isDesktopClient && (
|
||||
<SegmentedControl
|
||||
size="xs"
|
||||
value={dockPosition}
|
||||
onChange={(value) => setDockPosition(value as DesktopDockPosition)}
|
||||
data={DOCK_OPTIONS}
|
||||
aria-label="Dock position"
|
||||
/>
|
||||
)}
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size={30}
|
||||
onClick={closeBottomPanel}
|
||||
aria-label={`Close ${orientationLabel}`}
|
||||
title={`Close ${orientationLabel}`}
|
||||
aria-label="Close right dock"
|
||||
title="Close right dock"
|
||||
>
|
||||
{dockPosition === 'right' ? (
|
||||
<PanelRightClose className="h-4 w-4" aria-hidden="true" />
|
||||
) : (
|
||||
<PanelBottomClose className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
<PanelRightClose className="h-4 w-4" aria-hidden="true" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
|
|
|
|||
|
|
@ -8,23 +8,17 @@ import {
|
|||
useState,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
|
||||
export type DesktopBottomPanel = 'board-chat' | 'squad-chat';
|
||||
export type DesktopDockPosition = 'right' | 'bottom';
|
||||
|
||||
interface DesktopShellContextValue {
|
||||
isDesktopClient: boolean;
|
||||
leftRailOpen: boolean;
|
||||
rightRailOpen: boolean;
|
||||
bottomPanel: DesktopBottomPanel | null;
|
||||
dockPosition: DesktopDockPosition;
|
||||
bottomPanelHeight: number;
|
||||
rightPanelWidth: number;
|
||||
setLeftRailOpen: (open: boolean) => void;
|
||||
setRightRailOpen: (open: boolean) => void;
|
||||
setDockPosition: (position: DesktopDockPosition) => void;
|
||||
setBottomPanelHeight: (height: number) => void;
|
||||
setRightPanelWidth: (width: number) => void;
|
||||
openBottomPanel: (panel: DesktopBottomPanel) => void;
|
||||
closeBottomPanel: () => void;
|
||||
|
|
@ -38,13 +32,8 @@ const BOTTOM_PANEL_HEIGHT_STORAGE_KEY = 'veritas.workbench.bottomPanelHeight';
|
|||
const DOCK_POSITION_STORAGE_KEY = 'veritas.workbench.dockPosition';
|
||||
const RIGHT_PANEL_WIDTH_STORAGE_KEY = 'veritas.workbench.rightPanelWidth';
|
||||
const BOTTOM_PANEL_HISTORY_STATE_KEY = 'veritasBottomPanel';
|
||||
const BOTTOM_PANEL_VIEWPORT_RESERVE = 320;
|
||||
const RIGHT_PANEL_VIEWPORT_RESERVE = 520;
|
||||
const COMPACT_BOTTOM_PANEL_HEIGHT = 200;
|
||||
const COMPACT_RIGHT_PANEL_WIDTH = 280;
|
||||
export const DEFAULT_BOTTOM_PANEL_HEIGHT = 340;
|
||||
export const MIN_BOTTOM_PANEL_HEIGHT = 320;
|
||||
export const MAX_BOTTOM_PANEL_HEIGHT = 640;
|
||||
export const DEFAULT_RIGHT_PANEL_WIDTH = 420;
|
||||
export const MIN_RIGHT_PANEL_WIDTH = 320;
|
||||
export const MAX_RIGHT_PANEL_WIDTH = 640;
|
||||
|
|
@ -54,13 +43,9 @@ const DEFAULT_CONTEXT: DesktopShellContextValue = {
|
|||
leftRailOpen: false,
|
||||
rightRailOpen: false,
|
||||
bottomPanel: null,
|
||||
dockPosition: 'right',
|
||||
bottomPanelHeight: DEFAULT_BOTTOM_PANEL_HEIGHT,
|
||||
rightPanelWidth: DEFAULT_RIGHT_PANEL_WIDTH,
|
||||
setLeftRailOpen: () => undefined,
|
||||
setRightRailOpen: () => undefined,
|
||||
setDockPosition: () => undefined,
|
||||
setBottomPanelHeight: () => undefined,
|
||||
setRightPanelWidth: () => undefined,
|
||||
openBottomPanel: () => undefined,
|
||||
closeBottomPanel: () => undefined,
|
||||
|
|
@ -88,19 +73,6 @@ function readStoredBoolean(key: string, fallback: boolean): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
function clampBottomPanelHeight(height: number): number {
|
||||
const finiteHeight = Number.isFinite(height) ? height : DEFAULT_BOTTOM_PANEL_HEIGHT;
|
||||
const viewportMax =
|
||||
typeof window === 'undefined'
|
||||
? MAX_BOTTOM_PANEL_HEIGHT
|
||||
: Math.min(
|
||||
MAX_BOTTOM_PANEL_HEIGHT,
|
||||
Math.max(COMPACT_BOTTOM_PANEL_HEIGHT, window.innerHeight - BOTTOM_PANEL_VIEWPORT_RESERVE)
|
||||
);
|
||||
const viewportMin = Math.min(MIN_BOTTOM_PANEL_HEIGHT, viewportMax);
|
||||
return Math.min(viewportMax, Math.max(viewportMin, finiteHeight));
|
||||
}
|
||||
|
||||
function clampRightPanelWidth(width: number): number {
|
||||
const finiteWidth = Number.isFinite(width) ? width : DEFAULT_RIGHT_PANEL_WIDTH;
|
||||
const viewportMax =
|
||||
|
|
@ -114,21 +86,6 @@ function clampRightPanelWidth(width: number): number {
|
|||
return Math.min(viewportMax, Math.max(viewportMin, finiteWidth));
|
||||
}
|
||||
|
||||
function readStoredBottomPanelHeight(): number {
|
||||
if (typeof window === 'undefined') return DEFAULT_BOTTOM_PANEL_HEIGHT;
|
||||
|
||||
try {
|
||||
const stored = window.localStorage.getItem(BOTTOM_PANEL_HEIGHT_STORAGE_KEY);
|
||||
if (stored === null) return clampBottomPanelHeight(DEFAULT_BOTTOM_PANEL_HEIGHT);
|
||||
const value = Number(stored);
|
||||
return Number.isFinite(value) && value > 0
|
||||
? clampBottomPanelHeight(value)
|
||||
: clampBottomPanelHeight(DEFAULT_BOTTOM_PANEL_HEIGHT);
|
||||
} catch {
|
||||
return DEFAULT_BOTTOM_PANEL_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
function readStoredRightPanelWidth(): number {
|
||||
if (typeof window === 'undefined') return DEFAULT_RIGHT_PANEL_WIDTH;
|
||||
|
||||
|
|
@ -144,19 +101,6 @@ function readStoredRightPanelWidth(): number {
|
|||
}
|
||||
}
|
||||
|
||||
function readStoredDockPosition(): DesktopDockPosition {
|
||||
if (typeof window === 'undefined') return 'right';
|
||||
|
||||
try {
|
||||
const stored = window.localStorage.getItem(DOCK_POSITION_STORAGE_KEY);
|
||||
if (stored === 'right' || stored === 'bottom') return stored;
|
||||
if (stored !== null) window.localStorage.removeItem(DOCK_POSITION_STORAGE_KEY);
|
||||
} catch {
|
||||
// Use the safe desktop default when storage is unavailable.
|
||||
}
|
||||
return 'right';
|
||||
}
|
||||
|
||||
function writeStoredValue(key: string, value: string): void {
|
||||
try {
|
||||
window.localStorage.setItem(key, value);
|
||||
|
|
@ -179,8 +123,6 @@ function isBottomPanel(value: unknown): value is DesktopBottomPanel {
|
|||
|
||||
export function DesktopShellProvider({ children }: { children: ReactNode }) {
|
||||
const desktopClient = isDesktopClient();
|
||||
const supportsWorkbenchPanel = useMediaQuery('(min-width: 768px)', false);
|
||||
const canUseBottomPanel = desktopClient || supportsWorkbenchPanel;
|
||||
const panelTriggerRef = useRef<HTMLElement | null>(null);
|
||||
const [leftRailOpen, setLeftRailOpenState] = useState(() =>
|
||||
readStoredBoolean(LEFT_RAIL_STORAGE_KEY, true)
|
||||
|
|
@ -189,12 +131,6 @@ export function DesktopShellProvider({ children }: { children: ReactNode }) {
|
|||
readStoredBoolean(RIGHT_RAIL_STORAGE_KEY, false)
|
||||
);
|
||||
const [bottomPanel, setBottomPanel] = useState<DesktopBottomPanel | null>(null);
|
||||
const [dockPosition, setDockPositionState] = useState<DesktopDockPosition>(() =>
|
||||
readStoredDockPosition()
|
||||
);
|
||||
const [bottomPanelHeight, setBottomPanelHeightState] = useState(() =>
|
||||
readStoredBottomPanelHeight()
|
||||
);
|
||||
const [rightPanelWidth, setRightPanelWidthState] = useState(() => readStoredRightPanelWidth());
|
||||
|
||||
const setLeftRailOpen = useCallback(
|
||||
|
|
@ -213,20 +149,6 @@ export function DesktopShellProvider({ children }: { children: ReactNode }) {
|
|||
[desktopClient]
|
||||
);
|
||||
|
||||
const setDockPosition = useCallback(
|
||||
(position: DesktopDockPosition) => {
|
||||
setDockPositionState(position);
|
||||
if (desktopClient) writeStoredValue(DOCK_POSITION_STORAGE_KEY, position);
|
||||
},
|
||||
[desktopClient]
|
||||
);
|
||||
|
||||
const setBottomPanelHeight = useCallback((height: number) => {
|
||||
const next = clampBottomPanelHeight(height);
|
||||
setBottomPanelHeightState(next);
|
||||
writeStoredValue(BOTTOM_PANEL_HEIGHT_STORAGE_KEY, String(next));
|
||||
}, []);
|
||||
|
||||
const setRightPanelWidth = useCallback((width: number) => {
|
||||
const next = clampRightPanelWidth(width);
|
||||
setRightPanelWidthState(next);
|
||||
|
|
@ -290,8 +212,6 @@ export function DesktopShellProvider({ children }: { children: ReactNode }) {
|
|||
setLeftRailOpenState(true);
|
||||
setRightRailOpenState(false);
|
||||
setBottomPanel(null);
|
||||
setDockPositionState('right');
|
||||
setBottomPanelHeightState(clampBottomPanelHeight(DEFAULT_BOTTOM_PANEL_HEIGHT));
|
||||
setRightPanelWidthState(clampRightPanelWidth(DEFAULT_RIGHT_PANEL_WIDTH));
|
||||
removeStoredValue(LEFT_RAIL_STORAGE_KEY);
|
||||
removeStoredValue(RIGHT_RAIL_STORAGE_KEY);
|
||||
|
|
@ -317,6 +237,8 @@ export function DesktopShellProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
useEffect(() => {
|
||||
removeStoredValue(BOTTOM_PANEL_STORAGE_KEY);
|
||||
removeStoredValue(BOTTOM_PANEL_HEIGHT_STORAGE_KEY);
|
||||
removeStoredValue(DOCK_POSITION_STORAGE_KEY);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -331,13 +253,6 @@ export function DesktopShellProvider({ children }: { children: ReactNode }) {
|
|||
if (!isBottomPanel(panel)) restorePanelFocus();
|
||||
};
|
||||
const handleResize = () => {
|
||||
setBottomPanelHeightState((current) => {
|
||||
const next = clampBottomPanelHeight(current);
|
||||
if (next !== current) {
|
||||
writeStoredValue(BOTTOM_PANEL_HEIGHT_STORAGE_KEY, String(next));
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setRightPanelWidthState((current) => {
|
||||
const next = clampRightPanelWidth(current);
|
||||
if (next !== current) {
|
||||
|
|
@ -377,14 +292,10 @@ export function DesktopShellProvider({ children }: { children: ReactNode }) {
|
|||
isDesktopClient: desktopClient,
|
||||
leftRailOpen: desktopClient ? leftRailOpen : false,
|
||||
rightRailOpen: desktopClient ? rightRailOpen : false,
|
||||
bottomPanel: canUseBottomPanel ? bottomPanel : null,
|
||||
dockPosition: desktopClient ? dockPosition : 'bottom',
|
||||
bottomPanelHeight,
|
||||
bottomPanel: desktopClient ? bottomPanel : null,
|
||||
rightPanelWidth,
|
||||
setLeftRailOpen,
|
||||
setRightRailOpen,
|
||||
setDockPosition,
|
||||
setBottomPanelHeight,
|
||||
setRightPanelWidth,
|
||||
openBottomPanel,
|
||||
closeBottomPanel,
|
||||
|
|
@ -393,16 +304,11 @@ export function DesktopShellProvider({ children }: { children: ReactNode }) {
|
|||
[
|
||||
bottomPanel,
|
||||
closeBottomPanel,
|
||||
canUseBottomPanel,
|
||||
desktopClient,
|
||||
dockPosition,
|
||||
bottomPanelHeight,
|
||||
leftRailOpen,
|
||||
openBottomPanel,
|
||||
rightPanelWidth,
|
||||
rightRailOpen,
|
||||
setDockPosition,
|
||||
setBottomPanelHeight,
|
||||
setLeftRailOpen,
|
||||
setRightPanelWidth,
|
||||
setRightRailOpen,
|
||||
|
|
|
|||
|
|
@ -30,8 +30,6 @@ import {
|
|||
ListOrdered,
|
||||
MoreHorizontal,
|
||||
MessageSquare,
|
||||
PanelBottom,
|
||||
PanelBottomClose,
|
||||
PanelLeft,
|
||||
PanelLeftClose,
|
||||
PanelRight,
|
||||
|
|
@ -147,7 +145,6 @@ export function Header() {
|
|||
const [squadChatOpen, setSquadChatOpen] = useState(false);
|
||||
const [loadedPanels, setLoadedPanels] = useState<Set<LazyPanel>>(() => new Set());
|
||||
const isCompactHeader = useMediaQuery('(max-width: 639px)', false);
|
||||
const supportsWorkbenchPanel = useMediaQuery('(min-width: 768px)', false);
|
||||
const { setOpenCreateDialog, setOpenChatPanel } = useKeyboard();
|
||||
const { view, setView, navigateToTask } = useView();
|
||||
const { data: backlogCount = 0 } = useBacklogCount();
|
||||
|
|
@ -160,13 +157,12 @@ export function Header() {
|
|||
leftRailOpen,
|
||||
rightRailOpen,
|
||||
bottomPanel,
|
||||
dockPosition,
|
||||
setLeftRailOpen,
|
||||
setRightRailOpen,
|
||||
openBottomPanel,
|
||||
toggleBottomPanel,
|
||||
} = useDesktopShell();
|
||||
const usesWorkbenchChat = isDesktopClient || supportsWorkbenchPanel;
|
||||
const usesWorkbenchChat = isDesktopClient;
|
||||
const boardChatActive = usesWorkbenchChat && bottomPanel === 'board-chat';
|
||||
const squadChatActive = usesWorkbenchChat && bottomPanel === 'squad-chat';
|
||||
const boardChatAction = !usesWorkbenchChat
|
||||
|
|
@ -205,13 +201,13 @@ export function Header() {
|
|||
}, [markPanelLoaded]);
|
||||
|
||||
const openChatPanel = useCallback(() => {
|
||||
if (isDesktopClient || supportsWorkbenchPanel) {
|
||||
if (isDesktopClient) {
|
||||
openBottomPanel('board-chat');
|
||||
return;
|
||||
}
|
||||
markPanelLoaded('chat');
|
||||
setChatOpen(true);
|
||||
}, [isDesktopClient, markPanelLoaded, openBottomPanel, supportsWorkbenchPanel]);
|
||||
}, [isDesktopClient, markPanelLoaded, openBottomPanel]);
|
||||
|
||||
const toggleChatPanel = useCallback(() => {
|
||||
if (usesWorkbenchChat) {
|
||||
|
|
@ -231,13 +227,13 @@ export function Header() {
|
|||
);
|
||||
|
||||
const openSquadChatPanel = useCallback(() => {
|
||||
if (isDesktopClient || supportsWorkbenchPanel) {
|
||||
if (isDesktopClient) {
|
||||
openBottomPanel('squad-chat');
|
||||
return;
|
||||
}
|
||||
markPanelLoaded('squadChat');
|
||||
setSquadChatOpen(true);
|
||||
}, [isDesktopClient, markPanelLoaded, openBottomPanel, supportsWorkbenchPanel]);
|
||||
}, [isDesktopClient, markPanelLoaded, openBottomPanel]);
|
||||
|
||||
const toggleSquadChatPanel = useCallback(() => {
|
||||
if (usesWorkbenchChat) {
|
||||
|
|
@ -421,7 +417,7 @@ export function Header() {
|
|||
aria-label="Board actions"
|
||||
className="min-w-0 shrink-0"
|
||||
>
|
||||
{(isDesktopClient || supportsWorkbenchPanel) && (
|
||||
{isDesktopClient && (
|
||||
<Group gap={4} wrap="nowrap" className="desktop-no-drag">
|
||||
{isDesktopClient && (
|
||||
<>
|
||||
|
|
@ -462,16 +458,12 @@ export function Header() {
|
|||
color={bottomPanel ? 'veritas' : 'gray'}
|
||||
size={32}
|
||||
onClick={() => toggleBottomPanel('board-chat')}
|
||||
aria-label={bottomPanel ? `Close ${dockPosition} chat dock` : 'Open chat dock'}
|
||||
aria-label={bottomPanel ? 'Close right chat dock' : 'Open chat dock'}
|
||||
aria-pressed={Boolean(bottomPanel)}
|
||||
title={bottomPanel ? `Close ${dockPosition} chat dock` : 'Open chat dock'}
|
||||
title={bottomPanel ? 'Close right chat dock' : 'Open chat dock'}
|
||||
>
|
||||
{bottomPanel && dockPosition === 'bottom' ? (
|
||||
<PanelBottomClose className="h-4 w-4" aria-hidden="true" />
|
||||
) : bottomPanel ? (
|
||||
{bottomPanel ? (
|
||||
<PanelRightClose className="h-4 w-4" aria-hidden="true" />
|
||||
) : dockPosition === 'bottom' ? (
|
||||
<PanelBottom className="h-4 w-4" aria-hidden="true" />
|
||||
) : (
|
||||
<PanelRight className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -234,13 +234,19 @@ html[data-client='desktop'],
|
|||
html[data-client='desktop'] body,
|
||||
html[data-client='desktop'] #root {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
html[data-client='desktop'] .desktop-app-shell {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
min-height: 0;
|
||||
max-height: 100dvh;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
/* Keep tall board and drag descendants inside the app's internal scrollers.
|
||||
|
|
@ -286,19 +292,11 @@ html[data-client='desktop'] .desktop-workbench-container {
|
|||
min-height: 0;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
html[data-client='desktop'] .desktop-workbench-container[data-dock-position='right'] {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
html[data-client='desktop'] .desktop-workbench-container[data-dock-position='bottom'],
|
||||
html[data-client='desktop'] .desktop-workbench-container:not([data-dock-position]) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
html[data-client='desktop'] .desktop-workbench {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
|
|
@ -329,25 +327,8 @@ html[data-client='desktop'] .desktop-main-content {
|
|||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
html:not([data-client='desktop']) .workbench-chat-dock {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 35;
|
||||
height: var(--workbench-bottom-panel-height, 340px);
|
||||
max-height: min(max(200px, calc(100vh - 320px)), 640px);
|
||||
flex: 0 0 var(--workbench-bottom-panel-height, 340px);
|
||||
box-shadow: 0 -12px 32px color-mix(in oklch, var(--foreground) 10%, transparent);
|
||||
}
|
||||
|
||||
html[data-client='desktop'] .workbench-chat-dock--bottom {
|
||||
height: var(--workbench-bottom-panel-height, 340px);
|
||||
min-height: 0;
|
||||
max-height: min(max(200px, calc(100vh - 320px)), 640px);
|
||||
flex: 0 0 var(--workbench-bottom-panel-height, 340px);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
html[data-client='desktop'] .workbench-chat-dock--right {
|
||||
position: relative;
|
||||
width: var(--workbench-right-panel-width, 420px);
|
||||
min-width: 0;
|
||||
max-width: min(max(280px, calc(100vw - 520px)), 640px);
|
||||
|
|
@ -367,13 +348,6 @@ html[data-client='desktop'] .workbench-chat-dock--right {
|
|||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.workbench-chat-dock--bottom .workbench-chat-dock-resizer {
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
border-bottom-width: 1px;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.workbench-chat-dock--right .workbench-chat-dock-resizer {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
|
|
@ -384,10 +358,6 @@ html[data-client='desktop'] .workbench-chat-dock--right {
|
|||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.workbench-chat-dock--right {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.workbench-chat-dock-resizer:hover,
|
||||
.workbench-chat-dock-resizer:focus-visible {
|
||||
background: color-mix(in oklch, var(--primary) 12%, var(--card));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue