feat(manacore/web): add 2D resize (width + height) to workbench pages

Extend PageShell resize handle to support diagonal drag for both
width and height. Height is persisted per page in workbench settings.
Resize cursor changed from ew-resize to nwse-resize.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-02 23:49:39 +02:00
parent ef0c834a2b
commit b66a26810f
4 changed files with 55 additions and 19 deletions

View file

@ -12,6 +12,7 @@
minimized: boolean;
maximized?: boolean;
widthPx: number;
heightPx?: number;
title: string;
color: string;
}

View file

@ -10,11 +10,12 @@
interface Props {
widthPx: number;
heightPx?: number;
maximized?: boolean;
onClose: () => void;
onMinimize?: () => void;
onMaximize?: () => void;
onResize?: (widthPx: number) => void;
onResize?: (widthPx: number, heightPx?: number) => void;
// Default header
title?: string;
color?: string;
@ -28,6 +29,7 @@
let {
widthPx,
heightPx,
maximized = false,
onClose,
onMinimize,
@ -44,20 +46,31 @@
const MIN_WIDTH = 280;
const MAX_WIDTH = 1200;
const MIN_HEIGHT = 200;
const MAX_HEIGHT = 2000;
let resizing = $state(false);
function handleResizeStart(startX: number) {
function handleResizeStart(startX: number, startY: number) {
if (!onResize) return;
const startWidth = widthPx;
const startHeight = heightPx ?? 0;
resizing = true;
document.body.style.userSelect = 'none';
document.body.style.cursor = 'ew-resize';
document.body.style.cursor = 'nwse-resize';
function onMove(clientX: number) {
const delta = clientX - startX;
const newWidth = Math.round(Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth + delta)));
onResize!(newWidth);
function onMove(clientX: number, clientY: number) {
const deltaX = clientX - startX;
const newWidth = Math.round(Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidth + deltaX)));
if (startHeight > 0) {
const deltaY = clientY - startY;
const newHeight = Math.round(
Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, startHeight + deltaY))
);
onResize!(newWidth, newHeight);
} else {
onResize!(newWidth);
}
}
function onEnd() {
@ -71,10 +84,10 @@
}
function onMouseMove(e: MouseEvent) {
onMove(e.clientX);
onMove(e.clientX, e.clientY);
}
function onTouchMove(e: TouchEvent) {
onMove(e.touches[0].clientX);
onMove(e.touches[0].clientX, e.touches[0].clientY);
}
window.addEventListener('mousemove', onMouseMove);
@ -85,12 +98,12 @@
function onMouseDown(e: MouseEvent) {
e.preventDefault();
handleResizeStart(e.clientX);
handleResizeStart(e.clientX, e.clientY);
}
function onTouchStartHandle(e: TouchEvent) {
e.preventDefault();
handleResizeStart(e.touches[0].clientX);
handleResizeStart(e.touches[0].clientX, e.touches[0].clientY);
}
</script>
@ -98,7 +111,9 @@
class="page-shell"
class:maximized
class:resizing
style="width: {maximized ? '100%' : `${widthPx}px`}"
style="width: {maximized ? '100%' : `${widthPx}px`}; {heightPx && !maximized
? `height: ${heightPx}px; min-height: 0;`
: ''}"
>
<div class="drag-handle-bar">
<span class="drag-handle"><DotsSixVertical size={14} /></span>
@ -334,7 +349,7 @@
display: flex;
align-items: center;
justify-content: center;
cursor: ew-resize;
cursor: nwse-resize;
color: #d1d5db;
transition: color 0.15s;
border-radius: 0.25rem 0 0.375rem 0;

View file

@ -12,16 +12,18 @@
interface Props {
appId: string;
widthPx: number;
heightPx?: number;
maximized?: boolean;
onClose: () => void;
onMinimize?: () => void;
onMaximize?: () => void;
onResize?: (widthPx: number) => void;
onResize?: (widthPx: number, heightPx?: number) => void;
}
let {
appId,
widthPx,
heightPx,
maximized = false,
onClose,
onMinimize,
@ -165,6 +167,7 @@
<!-- Base: PageShell with list view (always visible) -->
<PageShell
{widthPx}
{heightPx}
{maximized}
title={appName}
color={appColor}

View file

@ -9,7 +9,13 @@
const DEFAULT_WIDTH = 480;
interface WorkbenchSettings extends Record<string, unknown> {
openApps: { appId: string; minimized: boolean; maximized?: boolean; widthPx?: number }[];
openApps: {
appId: string;
minimized: boolean;
maximized?: boolean;
widthPx?: number;
heightPx?: number;
}[];
}
const workbenchStore = createAppSettingsStore<WorkbenchSettings>('workbench-settings', {
@ -21,7 +27,13 @@
});
let openApps = $state<
{ appId: string; minimized: boolean; maximized?: boolean; widthPx?: number }[]
{
appId: string;
minimized: boolean;
maximized?: boolean;
widthPx?: number;
heightPx?: number;
}[]
>([
{ appId: 'todo', minimized: false },
{ appId: 'calendar', minimized: false },
@ -40,6 +52,7 @@
minimized: a.minimized,
maximized: a.maximized,
widthPx: a.widthPx,
heightPx: a.heightPx,
})),
});
}
@ -53,6 +66,7 @@
minimized: a.minimized,
maximized: a.maximized,
widthPx: a.widthPx ?? DEFAULT_WIDTH,
heightPx: a.heightPx,
title: entry?.name ?? a.appId,
color: entry?.color ?? '#6B7280',
};
@ -94,8 +108,10 @@
persistState();
}
function handleResize(id: string, widthPx: number) {
openApps = openApps.map((a) => (a.appId === id ? { ...a, widthPx } : a));
function handleResize(id: string, widthPx: number, heightPx?: number) {
openApps = openApps.map((a) =>
a.appId === id ? { ...a, widthPx, ...(heightPx !== undefined ? { heightPx } : {}) } : a
);
persistState();
}
@ -131,11 +147,12 @@
<AppPage
appId={p.id}
widthPx={p.widthPx}
heightPx={p.heightPx}
maximized={p.maximized}
onClose={() => handleRemoveApp(p.id)}
onMinimize={() => handleMinimizeApp(p.id)}
onMaximize={() => handleMaximizeApp(p.id)}
onResize={(w) => handleResize(p.id, w)}
onResize={(w, h) => handleResize(p.id, w, h)}
/>
{/snippet}
{#snippet picker()}