@@ -1882,6 +1910,8 @@ const DesktopWorkspaceView = () => {
if (dropActive) itemClasses.push('is-tag-target');
if (dropPending) itemClasses.push('is-tag-pending');
if (!matchesFilter) itemClasses.push('is-filtered-out');
+ const isSelected = selectedDocumentIds.includes(doc.id);
+ if (isSelected) itemClasses.push('is-selected');
const docTagTokens = docTagKeys.join(' ');
return (
{
itemRefs.current.delete(doc.id);
}
}}
- onPointerDown={(event) => handlePointerDown(event, doc.id)}
+ onPointerDown={(event) => {
+ if (typeof onDocumentPointerSelect === 'function') {
+ onDocumentPointerSelect(doc.id, event);
+ }
+ handlePointerDown(event, doc.id);
+ }}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerCancel}
@@ -1983,6 +2018,8 @@ export const createDesktopSurface = ({
renderSidebarToggle,
parentBreadcrumb,
onNavigateParent,
+ detailProps = null,
+ detailOpen = false,
}) => {
if (!workspaceProps) {
return null;
@@ -2007,6 +2044,7 @@ export const createDesktopSurface = ({
});
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
+ const detail = detailOpen && detailProps ?
: null;
const surfaceConfig = createWorkspaceSurfaceConfig({
key: 'workspace',
variant: 'workspace',
@@ -2018,10 +2056,11 @@ export const createDesktopSurface = ({
actions,
breadcrumbs: workspaceProps?.breadcrumbs || null,
content:
,
+ detail,
});
return {
...surfaceConfig,
- supportsDetail: false,
+ supportsDetail: Boolean(detailProps),
};
};
diff --git a/frontend/src/app/AppLayout.jsx b/frontend/src/app/AppLayout.jsx
index 3096809..01c0994 100644
--- a/frontend/src/app/AppLayout.jsx
+++ b/frontend/src/app/AppLayout.jsx
@@ -4791,6 +4791,34 @@ const AppLayout = () => {
],
);
+ const handleDeskInspectDocument = useCallback(
+ (docId) => {
+ if (!docId) {
+ return;
+ }
+ const rowKey = resolveDocumentRowKey(docId);
+ if (rowKey) {
+ applySelection([rowKey], { anchor: rowKey, interactedKeys: [rowKey] });
+ }
+ openDetailPanel({ documentIds: [docId] });
+ },
+ [applySelection, openDetailPanel],
+ );
+
+ const handleDeskDocumentPointerSelect = useCallback(
+ (docId, event) => {
+ if (!docId) {
+ return;
+ }
+ const rowKey = resolveDocumentRowKey(docId);
+ if (!rowKey) {
+ return;
+ }
+ handleRowSelection(rowKey, event);
+ },
+ [handleRowSelection],
+ );
+
const deskWorkspaceProps = useMemo(
() => ({
documents,
@@ -4802,6 +4830,10 @@ const AppLayout = () => {
onExit: handleDeskExit,
onRefresh: refreshCurrentFolder,
onDocumentOpen: openDocumentPreview,
+ onInspectDocument: handleDeskInspectDocument,
+ onDocumentPointerSelect: handleDeskDocumentPointerSelect,
+ selectedDocumentIds,
+ onClearSelection: clearDocumentSelection,
resolveThumbnailUrl: resolveThumbnailUrlForDoc,
onAssignTagToDocument: handleDocumentTagAttach,
onRemoveTagFromDocument: handleTagRemove,
@@ -4819,6 +4851,10 @@ const AppLayout = () => {
handleDeskExit,
refreshCurrentFolder,
openDocumentPreview,
+ handleDeskInspectDocument,
+ handleDeskDocumentPointerSelect,
+ selectedDocumentIds,
+ clearDocumentSelection,
resolveThumbnailUrlForDoc,
handleDocumentTagAttach,
handleTagRemove,
diff --git a/frontend/src/app/useWorkspaceSurface.js b/frontend/src/app/useWorkspaceSurface.js
index eaee1ba..7912f16 100644
--- a/frontend/src/app/useWorkspaceSurface.js
+++ b/frontend/src/app/useWorkspaceSurface.js
@@ -132,8 +132,18 @@ export const useWorkspaceSurface = ({
renderSidebarToggle,
parentBreadcrumb,
onNavigateParent: parentBreadcrumb ? onNavigateParent : null,
+ detailProps: detailPanelProps,
+ detailOpen: detailPanelOpen,
});
- }, [viewMode, deskWorkspaceProps, renderSidebarToggle, parentBreadcrumb, onNavigateParent]);
+ }, [
+ viewMode,
+ deskWorkspaceProps,
+ renderSidebarToggle,
+ parentBreadcrumb,
+ onNavigateParent,
+ detailPanelProps,
+ detailPanelOpen,
+ ]);
const surface = useMemo(() => {
if (showPreviewWorkspace) {
diff --git a/frontend/src/desktop/useDocumentDrag.js b/frontend/src/desktop/useDocumentDrag.js
index 57ce13c..5eb5704 100644
--- a/frontend/src/desktop/useDocumentDrag.js
+++ b/frontend/src/desktop/useDocumentDrag.js
@@ -2,6 +2,7 @@ import { useCallback, useRef } from 'react';
import { useDesktopContext } from './context';
import { preventAll } from './events';
import { clamp, formatTransform } from './math';
+import usePointerTap from '../ui/usePointerTap';
const DRAG_HYSTERESIS_PX = 4;
const DRAG_HYSTERESIS_SQUARED = DRAG_HYSTERESIS_PX * DRAG_HYSTERESIS_PX;
@@ -36,8 +37,33 @@ const useDocumentDrag = () => {
recalcVisibleDocIds,
settings,
containerRef,
+ onDocumentOpen,
+ onInspectDocument,
} = useDesktopContext();
+ const tapHandler = usePointerTap({
+ delay: 220,
+ onSingle: ({ data, event }) => {
+ if (!data || !data.docId) {
+ return;
+ }
+ if (event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) {
+ return;
+ }
+ openOverlayForDoc(data.docId, data.originInfo);
+ },
+ onDouble: ({ data }) => {
+ if (!data || !data.docId) {
+ return;
+ }
+ if (typeof onInspectDocument === 'function') {
+ onInspectDocument(data.docId);
+ return;
+ }
+ onDocumentOpen?.(data.docId);
+ },
+ });
+
const dragStateRef = useRef(null);
const inertiaAnimationsRef = useRef(new Map());
const { canvasPadding, defaultCanvasWidth, defaultCanvasHeight, debugDrag } = settings;
@@ -197,7 +223,10 @@ const useDocumentDrag = () => {
Number.isFinite(baseScale) && baseScale > 0 ? baseScale : 1;
const defaultCenterX = canvasPadding + docWidth / 2;
const defaultCenterY = canvasPadding + docHeight / 2;
- bringToFront(docId);
+ const metaPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
+ if (!metaPressed) {
+ bringToFront(docId);
+ }
const entry = layoutRef.current.get(docId) || null;
const centerX = typeof entry?.centerX === 'number' ? entry.centerX : defaultCenterX;
const centerY = typeof entry?.centerY === 'number' ? entry.centerY : defaultCenterY;
@@ -338,8 +367,8 @@ const useDocumentDrag = () => {
const rotatedOffsetY =
state.localPointerOffsetX * sinRot + state.localPointerOffsetY * cosRot;
- const desiredCenterX = pointerCanvasX - rotatedOffsetX;
- const desiredCenterY = pointerCanvasY - rotatedOffsetY;
+ const previousCenterX = Number.isFinite(entry.centerX) ? entry.centerX : state.originCenterX;
+ const previousCenterY = Number.isFinite(entry.centerY) ? entry.centerY : state.originCenterY;
const absCos = Math.abs(cosRot);
const absSin = Math.abs(sinRot);
@@ -351,6 +380,18 @@ const useDocumentDrag = () => {
const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - rotatedHalfWidth);
const minCenterY = canvasPadding + rotatedHalfHeight;
const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - rotatedHalfHeight);
+
+ const pointerRelativePrevX = pointerCanvasX - previousCenterX;
+ const pointerRelativePrevY = pointerCanvasY - previousCenterY;
+ const cosInversePrev = Math.cos(-rotationRad);
+ const sinInversePrev = Math.sin(-rotationRad);
+ const pointerLocalPrevX = pointerRelativePrevX * cosInversePrev - pointerRelativePrevY * sinInversePrev;
+ const pointerLocalPrevY = pointerRelativePrevX * sinInversePrev + pointerRelativePrevY * cosInversePrev;
+ const pointerInsideRelativeToPrev =
+ Math.abs(pointerLocalPrevX) <= halfWidth && Math.abs(pointerLocalPrevY) <= halfHeight;
+
+ const desiredCenterX = pointerCanvasX - rotatedOffsetX;
+ const desiredCenterY = pointerCanvasY - rotatedOffsetY;
const clampedCenterX = clamp(desiredCenterX, minCenterX, maxCenterX);
const clampedCenterY = clamp(desiredCenterY, minCenterY, maxCenterY);
@@ -363,19 +404,30 @@ const useDocumentDrag = () => {
state.moved = true;
}
- let currentCenterX = clampedCenterX;
- let currentCenterY = clampedCenterY;
- const updated = { ...entry, centerX: currentCenterX, centerY: currentCenterY };
- layoutRef.current.set(state.docId, updated);
-
const collidedWithHorizontalEdge =
Math.abs(desiredCenterY - clampedCenterY) > EDGE_COLLISION_THRESHOLD;
const collidedWithVerticalEdge =
Math.abs(desiredCenterX - clampedCenterX) > EDGE_COLLISION_THRESHOLD;
const collidedWithEdge = collidedWithHorizontalEdge || collidedWithVerticalEdge;
+ let currentCenterX = clampedCenterX;
+ let currentCenterY = clampedCenterY;
+ if (collidedWithEdge && !pointerInsideRelativeToPrev) {
+ currentCenterX = previousCenterX;
+ currentCenterY = previousCenterY;
+ }
+
+ const updated = { ...entry, centerX: currentCenterX, centerY: currentCenterY };
+ layoutRef.current.set(state.docId, updated);
+
const offsetX = pointerCanvasX - currentCenterX;
const offsetY = pointerCanvasY - currentCenterY;
+ const cosInverseCurrent = Math.cos(-rotationRad);
+ const sinInverseCurrent = Math.sin(-rotationRad);
+ const pointerLocalX = offsetX * cosInverseCurrent - offsetY * sinInverseCurrent;
+ const pointerLocalY = offsetX * sinInverseCurrent + offsetY * cosInverseCurrent;
+ const pointerInsideCard =
+ Math.abs(pointerLocalX) <= halfWidth && Math.abs(pointerLocalY) <= halfHeight;
const currentTimestamp =
typeof event.timeStamp === 'number' && Number.isFinite(event.timeStamp)
@@ -415,6 +467,14 @@ const useDocumentDrag = () => {
state.restRotation = nextRest;
}
dampingOverride = ANGULAR_DAMPING + EDGE_COLLISION_EXTRA_DAMPING;
+ if (!pointerInsideCard) {
+ torque = 0;
+ currentCenterX = entry?.centerX ?? currentCenterX;
+ currentCenterY = entry?.centerY ?? currentCenterY;
+ if (updated.centerX !== currentCenterX || updated.centerY !== currentCenterY) {
+ layoutRef.current.set(state.docId, { ...updated, centerX: currentCenterX, centerY: currentCenterY });
+ }
+ }
}
state.lastClientX = event.clientX;
@@ -522,6 +582,20 @@ const useDocumentDrag = () => {
}
}
}
+
+ const rotationForOffsetDeg = state.rotation || 0;
+ const rotationForOffsetRad = (rotationForOffsetDeg * Math.PI) / 180;
+ const cosInverse = Math.cos(-rotationForOffsetRad);
+ const sinInverse = Math.sin(-rotationForOffsetRad);
+ const pointerRelativeX = pointerCanvasX - currentCenterX;
+ const pointerRelativeY = pointerCanvasY - currentCenterY;
+ const updatedLocalOffsetX = pointerRelativeX * cosInverse - pointerRelativeY * sinInverse;
+ const updatedLocalOffsetY = pointerRelativeX * sinInverse + pointerRelativeY * cosInverse;
+ if (!collidedWithEdge || pointerInsideCard) {
+ state.localPointerOffsetX = updatedLocalOffsetX;
+ state.localPointerOffsetY = updatedLocalOffsetY;
+ }
+
if (debugDrag) {
console.log('[desk] handlePointerMove: moved doc', state.docId, 'to', currentCenterX, currentCenterY);
}
@@ -563,20 +637,35 @@ const useDocumentDrag = () => {
}
const docId = state.docId;
- bringToFront(docId);
+ const metaPressed = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
+ if (!metaPressed) {
+ bringToFront(docId);
+ }
const originInfo = {
rotation: state.rotation || 0,
scale: state.baseScale || 1,
width: state.width,
height: state.height,
};
+ const docKey = docId != null ? String(docId) : null;
+ const doc = docKey ? documentLookup.get(docKey) : null;
+ tapHandler(event, {
+ docId,
+ originInfo,
+ docTitle: doc?.title || 'document',
+ });
finishDrag(event.pointerId);
- openOverlayForDoc(docId, originInfo);
return;
}
finishDrag(event.pointerId);
},
- [bringToFront, finishDrag, openOverlayForDoc, startInertiaAnimation],
+ [
+ bringToFront,
+ documentLookup,
+ finishDrag,
+ startInertiaAnimation,
+ tapHandler,
+ ],
);
const handlePointerCancel = useCallback(
diff --git a/frontend/src/detail/DetailPanel.jsx b/frontend/src/detail/DetailPanel.jsx
index 20d5097..53094c0 100644
--- a/frontend/src/detail/DetailPanel.jsx
+++ b/frontend/src/detail/DetailPanel.jsx
@@ -879,7 +879,7 @@ const DetailPanel = ({
title="Open preview"
disabled={!singleHasPreview}
>
-
+
,
);
}
diff --git a/frontend/src/documents/workspaceHeader.js b/frontend/src/documents/workspaceHeader.js
index 43a4654..b98b407 100644
--- a/frontend/src/documents/workspaceHeader.js
+++ b/frontend/src/documents/workspaceHeader.js
@@ -1,12 +1,9 @@
import React from 'react';
-import { ArrowUpIcon } from '../ui/icons';
export const createWorkspaceSurfaceConfig = ({
title,
subtitle = null,
sidebarToggle = null,
- parentBreadcrumb = null,
- onNavigateParent = null,
actions = null,
breadcrumbs = null,
content = null,
@@ -14,25 +11,10 @@ export const createWorkspaceSurfaceConfig = ({
variant = 'documents',
key = 'documents',
}) => {
- const showParent = parentBreadcrumb && typeof onNavigateParent === 'function';
-
- const parentControl = showParent ? (
-
- ) : null;
-
- const leading = sidebarToggle || parentControl
+ const leading = sidebarToggle
? (
<>
{sidebarToggle}
- {parentControl}
>
)
: null;
diff --git a/frontend/src/sidebar/Sidebar.jsx b/frontend/src/sidebar/Sidebar.jsx
index 8565ca7..62037bc 100644
--- a/frontend/src/sidebar/Sidebar.jsx
+++ b/frontend/src/sidebar/Sidebar.jsx
@@ -65,6 +65,11 @@ const FolderNode = ({
className={rowClasses.join(' ')}
draggable={canDrag}
onClick={() => onSelect(node.id)}
+ onDoubleClick={() => {
+ if (canToggle) {
+ onToggle(node.id);
+ }
+ }}
onDragOver={(event) => onDragOver(event, node.id)}
onDragLeave={onDragLeave}
onDrop={(event) => onDrop(event, node.id)}
@@ -84,9 +89,11 @@ const FolderNode = ({
>
{icon}
-
+
- {node.name}
+
+ {node.name}
+
{node.id !== 'root' && (
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index 66aebf6..f4b02cd 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -555,6 +555,10 @@ button.danger:hover:not([disabled]) {
justify-content: center;
}
+.icon--flip-y {
+ transform: scaleX(-1);
+}
+
.visually-hidden {
position: absolute;
width: 1px;
@@ -671,6 +675,10 @@ button.danger:hover:not([disabled]) {
flex: 1 1 auto;
}
+.main-content__body--workspace.main-content__body--has-detail > .desk-shell {
+ margin-right: calc(-1 * var(--detail-panel-width));
+}
+
.main-content--has-detail {
padding-right: var(--detail-panel-width);
}
@@ -1373,16 +1381,29 @@ button.danger:hover:not([disabled]) {
transition: background 0.12s ease, color 0.12s ease;
position: relative;
overflow: hidden;
+ user-select: none;
+ -webkit-user-select: none;
}
-.folder-row span.name {
+.folder-row .name-wrap {
display: inline-flex;
align-items: center;
gap: 0.25rem;
flex: 1;
+ min-width: 0;
+}
+
+.folder-row .name-wrap .name__label {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
white-space: nowrap;
}
+.folder-row .folder-icon-image {
+ flex-shrink: 0;
+}
+
.folder-row:hover {
background: var(--sidebar-hover-bg);
color: var(--fg);
@@ -2479,7 +2500,7 @@ button.danger:hover:not([disabled]) {
}
.panel.detail-panel {
- position: absolute;
+ position: fixed;
top: 0;
right: 0;
width: min(100vw, var(--detail-panel-width));
@@ -2491,7 +2512,7 @@ button.danger:hover:not([disabled]) {
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
- z-index: 20;
+ z-index: 1020;
}
.panel-header {