Skip to content

Commit d6882b3

Browse files
authored
improve mobile experience (#79)
1 parent 26d1a89 commit d6882b3

3 files changed

Lines changed: 109 additions & 21 deletions

File tree

src/components/MainControlView.css

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1782,15 +1782,15 @@ body.no-scroll {
17821782
.desktop-workspace .workspace-column-resize-handle {
17831783
position: relative;
17841784
width: 100%;
1785-
height: 18px;
1786-
min-height: 18px;
1785+
height: 32px;
1786+
min-height: 32px;
17871787
cursor: row-resize;
17881788
}
17891789

17901790
.desktop-workspace .workspace-row-resize-handle .workspace-resize-handle-bar,
17911791
.desktop-workspace .workspace-column-resize-handle .workspace-resize-handle-bar {
1792-
width: 64px;
1793-
height: 3px;
1792+
width: min(104px, 32vw);
1793+
height: 4px;
17941794
}
17951795

17961796
.desktop-workspace .workspace-column-resize-handle:hover .workspace-resize-handle-bar {
@@ -1803,9 +1803,9 @@ body.no-scroll {
18031803
left: 50%;
18041804
z-index: 2;
18051805
display: inline-grid;
1806-
width: 24px;
1807-
height: 24px;
1808-
min-width: 24px;
1806+
width: 30px;
1807+
height: 30px;
1808+
min-width: 30px;
18091809
padding: 0;
18101810
transform: translate(-50%, -50%);
18111811
place-items: center;

src/components/MainControlView.test.tsx

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,66 @@ describe('MainControlView desktop workspace', () => {
306306
fireEvent(screen.getByLabelText('Desktop workspace'), pointerUp);
307307

308308
await waitFor(() => {
309-
const stored = JSON.parse(localStorage.getItem(workspaceLayoutKey) || '{}');
310-
expect(stored.columnRatiosByRow[0][0]).toBeGreaterThan(stored.columnRatiosByRow[0][1]);
309+
const cards = row.querySelectorAll<HTMLElement>('.workspace-card');
310+
expect(parseFloat(cards[0].style.flex)).toBeGreaterThan(parseFloat(cards[1].style.flex));
311+
});
312+
313+
// A touch-only split must not overwrite the saved desktop column layout.
314+
const stored = JSON.parse(localStorage.getItem(workspaceLayoutKey) || '{}');
315+
expect(stored.columnRatiosByRow[0]).toEqual([1, 1]);
316+
});
317+
318+
it('reflows a multi-row desktop workspace cleanly when the viewport becomes mobile', async () => {
319+
let stackedListener: ((event: MediaQueryListEvent) => void) | undefined;
320+
Object.defineProperty(window, 'matchMedia', {
321+
writable: true,
322+
value: vi.fn((query: string) => ({
323+
matches: query === '(min-width: 1024px)',
324+
media: query,
325+
onchange: null,
326+
addListener: vi.fn(),
327+
removeListener: vi.fn(),
328+
addEventListener: vi.fn((type: string, listener: (event: MediaQueryListEvent) => void) => {
329+
if (query === '(max-width: 767px)' && type === 'change') stackedListener = listener;
330+
}),
331+
removeEventListener: vi.fn(),
332+
dispatchEvent: vi.fn(),
333+
})),
334+
});
335+
localStorage.setItem(
336+
workspacePanelsKey,
337+
JSON.stringify([
338+
makePanel('panel-camera', 'camera', 'Camera'),
339+
makePanel('panel-pad', 'pad', 'Pad controls'),
340+
makePanel('panel-bt', 'behaviorTree', 'Behavior tree'),
341+
makePanel('panel-3d', '3d', '3D view'),
342+
])
343+
);
344+
localStorage.setItem(workspaceTileOrderKey, JSON.stringify(['panel-camera', 'panel-pad', 'panel-bt', 'panel-3d']));
345+
localStorage.setItem(
346+
workspaceLayoutKey,
347+
JSON.stringify({
348+
rowSizes: [1, 1, 2],
349+
rowRatios: [4, 2, 1],
350+
columnRatiosByRow: { 0: [1], 1: [1], 2: [3, 1] },
351+
})
352+
);
353+
354+
renderMainControlView();
355+
const workspace = await screen.findByLabelText('Desktop workspace');
356+
expect(workspace.querySelectorAll('.workspace-card')).toHaveLength(4);
357+
workspace.scrollTop = 120;
358+
359+
act(() => stackedListener?.({ matches: true } as MediaQueryListEvent));
360+
361+
await waitFor(() => {
362+
const rows = workspace.querySelectorAll('.workspace-tile-row');
363+
const cards = workspace.querySelectorAll<HTMLElement>('.workspace-card');
364+
expect(rows).toHaveLength(1);
365+
expect(cards).toHaveLength(2);
366+
expect(parseFloat(cards[0].style.flex)).toBe(0.5);
367+
expect(parseFloat(cards[1].style.flex)).toBe(0.5);
368+
expect(workspace.scrollTop).toBe(0);
311369
});
312370
});
313371

src/components/MainControlView.tsx

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ type WorkspaceInteraction =
429429
| {
430430
mode: 'column';
431431
axis: 'x' | 'y';
432+
layout: 'workspace' | 'stacked';
432433
rowIndex: number;
433434
index: number;
434435
startClientX: number;
@@ -1004,6 +1005,7 @@ const MainControlView: React.FC<MainControlViewProps> = ({ connectionParams, onD
10041005
if (typeof window === 'undefined' || !window.matchMedia) return false;
10051006
return window.matchMedia(STACKED_WORKSPACE_QUERY).matches;
10061007
});
1008+
const [stackedWorkspaceRatios, setStackedWorkspaceRatios] = useState([1, 1]);
10071009
// Once BT panel mounts, keep it alive (preserves nodes/executor state)
10081010
const [btEverMounted, setBtEverMounted] = useState(false);
10091011
const [tfEverMounted, setTfEverMounted] = useState(false);
@@ -1089,8 +1091,7 @@ const MainControlView: React.FC<MainControlViewProps> = ({ connectionParams, onD
10891091
[workspaceLayout.rowSizes, workspaceTiles]
10901092
);
10911093
const renderedWorkspaceRows = useMemo(
1092-
() =>
1093-
isWorkspaceStacked ? (workspaceTiles.length > 0 ? [workspaceTiles.slice(0, 2)] : []) : workspaceRows,
1094+
() => (isWorkspaceStacked ? (workspaceTiles.length > 0 ? [workspaceTiles.slice(0, 2)] : []) : workspaceRows),
10941095
[isWorkspaceStacked, workspaceRows, workspaceTiles]
10951096
);
10961097
const workspaceRowRatios = useMemo(
@@ -1102,6 +1103,13 @@ const MainControlView: React.FC<MainControlViewProps> = ({ connectionParams, onD
11021103
workspaceRows.map((row, rowIndex) => normalizeRatios(workspaceLayout.columnRatiosByRow[rowIndex], row.length)),
11031104
[workspaceLayout.columnRatiosByRow, workspaceRows]
11041105
);
1106+
const renderedWorkspaceColumnRatiosByRow = useMemo<Record<number, number[]>>(() => {
1107+
if (isWorkspaceStacked) {
1108+
return { 0: normalizeRatios(stackedWorkspaceRatios, renderedWorkspaceRows[0]?.length || 0) };
1109+
}
1110+
1111+
return Object.fromEntries(workspaceColumnRatiosByRow.map((ratios, rowIndex) => [rowIndex, ratios]));
1112+
}, [isWorkspaceStacked, renderedWorkspaceRows, stackedWorkspaceRatios, workspaceColumnRatiosByRow]);
11051113
const capturedWorkspaceLayout = useMemo<WorkspaceLayoutState>(() => {
11061114
const rowSizes = workspaceRows.map(row => row.length);
11071115
const columnRatiosByRow = rowSizes.reduce<Record<number, number[]>>((ratiosByRow, rowSize, rowIndex) => {
@@ -1251,6 +1259,20 @@ const MainControlView: React.FC<MainControlViewProps> = ({ connectionParams, onD
12511259
return () => mediaQuery.removeEventListener('change', handleMediaChange);
12521260
}, []);
12531261

1262+
useEffect(() => {
1263+
if (!isWorkspaceStacked) return;
1264+
1265+
// The mobile layout combines panels that may have belonged to different
1266+
// desktop rows. Start it at the top and notify canvas-based children only
1267+
// after that new geometry has committed.
1268+
if (workspaceRef.current) workspaceRef.current.scrollTop = 0;
1269+
workspaceInteractionRef.current = null;
1270+
setIsWorkspaceResizing(false);
1271+
1272+
const id = window.setTimeout(() => window.dispatchEvent(new Event('resize')), 0);
1273+
return () => window.clearTimeout(id);
1274+
}, [isWorkspaceStacked]);
1275+
12541276
useEffect(() => {
12551277
if (!isWorkspaceTemplateMenuOpen) return;
12561278

@@ -1857,12 +1879,13 @@ const MainControlView: React.FC<MainControlViewProps> = ({ connectionParams, onD
18571879
workspaceInteractionRef.current = {
18581880
mode: 'column',
18591881
axis,
1882+
layout: isWorkspaceStacked ? 'stacked' : 'workspace',
18601883
rowIndex,
18611884
index,
18621885
startClientX: event.clientX,
18631886
startClientY: event.clientY,
18641887
containerSize: (axis === 'y' ? rowElement?.clientHeight : rowElement?.clientWidth) || 1,
1865-
startRatios: workspaceColumnRatiosByRow[rowIndex] || [],
1888+
startRatios: renderedWorkspaceColumnRatiosByRow[rowIndex] || [],
18661889
};
18671890
};
18681891

@@ -1881,16 +1904,23 @@ const MainControlView: React.FC<MainControlViewProps> = ({ connectionParams, onD
18811904
return;
18821905
}
18831906

1907+
const nextRatios = updateAdjacentRatios(
1908+
interaction.startRatios,
1909+
interaction.index,
1910+
interaction.axis === 'y' ? deltaY : deltaX,
1911+
interaction.containerSize
1912+
);
1913+
1914+
if (interaction.layout === 'stacked') {
1915+
setStackedWorkspaceRatios(nextRatios);
1916+
return;
1917+
}
1918+
18841919
setWorkspaceLayout(prev => ({
18851920
...prev,
18861921
columnRatiosByRow: {
18871922
...prev.columnRatiosByRow,
1888-
[interaction.rowIndex]: updateAdjacentRatios(
1889-
interaction.startRatios,
1890-
interaction.index,
1891-
interaction.axis === 'y' ? deltaY : deltaX,
1892-
interaction.containerSize
1893-
),
1923+
[interaction.rowIndex]: nextRatios,
18941924
},
18951925
}));
18961926
}, []);
@@ -3453,7 +3483,7 @@ const MainControlView: React.FC<MainControlViewProps> = ({ connectionParams, onD
34533483
data-workspace-card-id={tile.id}
34543484
data-workspace-row-index={rowIndex}
34553485
data-workspace-column-index={columnIndex}
3456-
style={{ flex: workspaceColumnRatiosByRow[rowIndex]?.[columnIndex] || 1 }}
3486+
style={{ flex: renderedWorkspaceColumnRatiosByRow[rowIndex]?.[columnIndex] || 1 }}
34573487
>
34583488
<header
34593489
className="workspace-card-header"
@@ -3490,7 +3520,7 @@ const MainControlView: React.FC<MainControlViewProps> = ({ connectionParams, onD
34903520
data-workspace-card-id={tile.id}
34913521
data-workspace-row-index={rowIndex}
34923522
data-workspace-column-index={columnIndex}
3493-
style={{ flex: workspaceColumnRatiosByRow[rowIndex]?.[columnIndex] || 1 }}
3523+
style={{ flex: renderedWorkspaceColumnRatiosByRow[rowIndex]?.[columnIndex] || 1 }}
34943524
>
34953525
<header
34963526
className="workspace-card-header"
@@ -3525,7 +3555,7 @@ const MainControlView: React.FC<MainControlViewProps> = ({ connectionParams, onD
35253555
data-workspace-card-id={tile.panel.id}
35263556
data-workspace-row-index={rowIndex}
35273557
data-workspace-column-index={columnIndex}
3528-
style={{ flex: workspaceColumnRatiosByRow[rowIndex]?.[columnIndex] || 1 }}
3558+
style={{ flex: renderedWorkspaceColumnRatiosByRow[rowIndex]?.[columnIndex] || 1 }}
35293559
onAnimationEnd={() => {
35303560
setLastAddedWorkspacePanelId(prev => (prev === tile.panel.id ? null : prev));
35313561
setExecutionJumpPanelId(prev => (prev === tile.panel.id ? null : prev));

0 commit comments

Comments
 (0)