mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
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:
parent
ef0c834a2b
commit
b66a26810f
4 changed files with 55 additions and 19 deletions
|
|
@ -12,6 +12,7 @@
|
|||
minimized: boolean;
|
||||
maximized?: boolean;
|
||||
widthPx: number;
|
||||
heightPx?: number;
|
||||
title: string;
|
||||
color: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue