From 91dd3289a9ed1301f03587dd0c9e24069469c02f Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Fri, 31 Oct 2025 14:27:30 +0100 Subject: [PATCH] deskview --- frontend/src/DesktopWorkspace.css | 60 +++++------ frontend/src/DesktopWorkspace.jsx | 122 +++++++++++----------- frontend/src/app/AppLayout.jsx | 53 ++++++---- frontend/src/app/DocumentsRoute.jsx | 8 +- frontend/src/app/useWorkspaceSurface.js | 16 +-- frontend/src/desktop/useDocumentDrag.js | 14 +-- frontend/src/documents/DocumentsTable.jsx | 43 +++++--- frontend/src/styles.css | 4 +- 8 files changed, 169 insertions(+), 151 deletions(-) diff --git a/frontend/src/DesktopWorkspace.css b/frontend/src/DesktopWorkspace.css index 352d31a..7c0192e 100644 --- a/frontend/src/DesktopWorkspace.css +++ b/frontend/src/DesktopWorkspace.css @@ -1,12 +1,12 @@ /* Desktop workspace styles */ -.skeuo-main { +.desk-main { flex: 1; display: flex; flex-direction: column; min-height: 0; } -.skeuo-item { +.desk-item { position: absolute; display: block; width: auto; @@ -18,7 +18,7 @@ will-change: transform; } -.skeuo-shell { +.desk-shell { flex: 1; display: flex; flex-direction: column; @@ -27,14 +27,14 @@ position: relative; } -.skeuo-canvas { +.desk-canvas { flex: 1; position: relative; overflow: hidden; margin: 0; } -.skeuo-empty { +.desk-empty { height: 100%; display: flex; align-items: center; @@ -47,32 +47,32 @@ -.skeuo-item__body { +.desk-item__body { flex-grow: 1; width: 100%; height: 100%; } -.skeuo-item:focus-visible { +.desk-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; } -.skeuo-item.is-dragging { +.desk-item.is-dragging { cursor: grabbing; transition: none; } -.skeuo-item.is-tag-target .skeuo-item__card { +.desk-item.is-tag-target .desk-item__card { outline: 0.35rem dashed var(--accent); outline-offset: 0.35rem; } -.skeuo-item.is-tag-pending .skeuo-item__card { +.desk-item.is-tag-pending .desk-item__card { opacity: 0.6; } -.skeuo-item.is-filtered-out { +.desk-item.is-filtered-out { opacity: 0.12; pointer-events: none; filter: blur(15px) grayscale(100%); @@ -80,7 +80,7 @@ z-index: 0 !important; } -.skeuo-item__tags { +.desk-item__tags { --tag-scale: 1; position: absolute; top: 0; @@ -112,14 +112,14 @@ pointer-events: none; } -.skeuo-item__tags .tag-chip { +.desk-item__tags .tag-chip { font-size: 0.85rem; padding: 0.18rem 0.55rem; gap: 0.3rem; } -.skeuo-card__nav { +.desk-card__nav { position: absolute; bottom: 1.8rem; left: 50%; @@ -132,12 +132,12 @@ transition: opacity 0.2s ease; } -.skeuo-item__card:hover .skeuo-card__nav { +.desk-item__card:hover .desk-card__nav { opacity: 1; pointer-events: auto; } -.skeuo-card__nav-button { +.desk-card__nav-button { display: inline-flex; align-items: center; justify-content: center; @@ -152,39 +152,39 @@ transition: background 0.15s ease, opacity 0.15s ease; } -.skeuo-card__nav-button:hover:not([disabled]) { +.desk-card__nav-button:hover:not([disabled]) { background: var(--preview-nav-bg-hover); } -.skeuo-card__nav-button:disabled { +.desk-card__nav-button:disabled { opacity: 0.4; cursor: default; } -.skeuo-card__nav-button:focus-visible { +.desk-card__nav-button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } -.skeuo-card__nav-button svg { +.desk-card__nav-button svg { width: 100%; height: 100%; } -.skeuo-item__tags .tag-chip--tear-pending { +.desk-item__tags .tag-chip--tear-pending { opacity: 0.35; } -body.skeuo-cursor-remove, -body.skeuo-cursor-remove * { +body.desk-cursor-remove, +body.desk-cursor-remove * { cursor: not-allowed !important; } -.skeuo-item__shadow { +.desk-item__shadow { display: none; } -.skeuo-item__card { +.desk-item__card { position: relative; border-radius: 0; display: flex; @@ -196,14 +196,14 @@ body.skeuo-cursor-remove * { overflow: hidden; } -.skeuo-item__card img { +.desk-item__card img { width: 100%; height: 100%; object-fit: contain; display: block; } -.skeuo-item__card--empty { +.desk-item__card--empty { box-shadow: 0 12px 32px var(--shadow-medium); background: radial-gradient(circle at 42% 38%, color-mix(in oklch, var(--surface-subtle) 75%, var(--selection) 25%), color-mix(in oklch, var(--surface-subtle) 85%, var(--selection) 15%) 70%), @@ -217,14 +217,14 @@ body.skeuo-cursor-remove * { gap: 0.5rem; } -.skeuo-item__placeholder { +.desk-item__placeholder { font-size: 0.95rem; font-weight: 500; letter-spacing: normal; color: var(--muted); } -.skeuo-item__empty { +.desk-item__empty { display: flex; flex-direction: column; align-items: center; @@ -236,7 +236,7 @@ body.skeuo-cursor-remove * { text-align: center; } -.skeuo-item__title { +.desk-item__title { font-size: 0.95rem; font-weight: 500; color: var(--fg); diff --git a/frontend/src/DesktopWorkspace.jsx b/frontend/src/DesktopWorkspace.jsx index ef9ed5b..6339f85 100644 --- a/frontend/src/DesktopWorkspace.jsx +++ b/frontend/src/DesktopWorkspace.jsx @@ -8,7 +8,8 @@ import React, { } from 'react'; import { resolveDocumentAssetUrl, createAssetView } from './asset_manager'; import { useAssetNavigator } from './hooks/useAssetNavigator'; -import { ArrowLeftIcon, ArrowRightIcon, RefreshIcon } from './ui/icons'; +import { ArrowLeftIcon, ArrowRightIcon } from './ui/icons'; +import { createDocumentsTableHeaderActions } from './documents/DocumentsTable'; import { clamp, formatTransform } from './desktop/math'; import { preventAll } from './desktop/events'; import useDocumentDrag from './desktop/useDocumentDrag'; @@ -122,7 +123,7 @@ const readTransferData = (dataTransfer, mimeTypes) => { } } catch (error) { if (DEBUG_DROP) { - console.warn('[skeuo] readTransferData failed for type', type, error); + console.warn('[desk] readTransferData failed for type', type, error); } } } @@ -140,7 +141,7 @@ const parseTagTransferPayload = (event) => { try { return JSON.parse(raw); } catch (error) { - console.warn('[skeuo] parseTagTransferPayload failed', error); + console.warn('[desk] parseTagTransferPayload failed', error); } return null; }; @@ -207,8 +208,8 @@ const DesktopPreviewCard = ({ onNavigatorSnapshot, ]); const hasPreview = Boolean(currentUrl); - const cardClasses = ['skeuo-item__card']; - if (!hasPreview) cardClasses.push('skeuo-item__card--empty'); + const cardClasses = ['desk-item__card']; + if (!hasPreview) cardClasses.push('desk-item__card--empty'); const showNav = hasPreview && (cardinality > 1 || canGoPrev || canGoNext); return ( @@ -216,18 +217,18 @@ const DesktopPreviewCard = ({ {hasPreview ? ( {title} ) : ( -
-
DOC
-
+
+
DOC
+
{title}
)} {showNav ? ( -
+
- - - ); + const actions = createDocumentsTableHeaderActions({ + viewMode: viewMode || 'desk', + onViewModeChange, + onRefresh, + }); return { key: 'workspace', diff --git a/frontend/src/app/AppLayout.jsx b/frontend/src/app/AppLayout.jsx index 4bd5411..d04570f 100644 --- a/frontend/src/app/AppLayout.jsx +++ b/frontend/src/app/AppLayout.jsx @@ -190,14 +190,14 @@ const AppLayout = () => { const [currentFolder, setCurrentFolder] = useState(null); const [currentSubfolders, setCurrentSubfolders] = useState([]); const [documents, setDocuments] = useState([]); - const [workspaceMode, setWorkspaceMode] = useState('table'); const [documentsViewMode, setDocumentsViewMode] = useState(() => { if (typeof window === 'undefined') { return 'list'; } const stored = window.localStorage.getItem('papercrate_view_mode'); - return stored === 'grid' ? 'grid' : 'list'; + return stored === 'grid' || stored === 'desk' ? stored : 'list'; }); + const lastNonDeskViewRef = useRef(documentsViewMode === 'desk' ? 'list' : documentsViewMode); const loadThemeSettings = () => { const defaults = { neutralHue: 260, mode: 'system' }; @@ -258,6 +258,12 @@ const AppLayout = () => { const initialThemeSettings = loadThemeSettings(); const [neutralHue, setNeutralHue] = useState(initialThemeSettings.neutralHue); const [themeMode, setThemeMode] = useState(initialThemeSettings.mode); + + useEffect(() => { + if (documentsViewMode !== 'desk') { + lastNonDeskViewRef.current = documentsViewMode; + } + }, [documentsViewMode]); const initialRowSelection = []; const [selectedRowKeys, setSelectedRowKeys] = useState(initialRowSelection); const [selectionOrder, setSelectionOrder] = useState(initialRowSelection); @@ -281,9 +287,9 @@ const AppLayout = () => { } tagRemovalCursorActiveRef.current = active; if (active) { - body.classList.add('skeuo-cursor-remove'); + body.classList.add('desk-cursor-remove'); } else { - body.classList.remove('skeuo-cursor-remove'); + body.classList.remove('desk-cursor-remove'); } }, []); const refreshAccessToken = useCallback(async () => { @@ -4641,11 +4647,9 @@ const AppLayout = () => { [ensureAssetUrl, getDocumentAsset], ); - const showSkeuoWorkspace = useCallback(() => setWorkspaceMode('skeuo'), [setWorkspaceMode]); - const exitSkeuoWorkspace = useCallback(() => setWorkspaceMode('table'), [setWorkspaceMode]); const handleDocumentsViewModeChange = useCallback((mode) => { + const next = mode === 'grid' ? 'grid' : mode === 'desk' ? 'desk' : 'list'; setDocumentsViewMode((previous) => { - const next = mode === 'grid' ? 'grid' : 'list'; if (next !== previous && typeof window !== 'undefined') { window.localStorage.setItem('papercrate_view_mode', next); } @@ -4653,6 +4657,13 @@ const AppLayout = () => { }); }, []); + const handleDeskExit = useCallback(() => { + const fallback = lastNonDeskViewRef.current && lastNonDeskViewRef.current !== 'desk' + ? lastNonDeskViewRef.current + : 'list'; + handleDocumentsViewModeChange(fallback); + }, [handleDocumentsViewModeChange]); + const handleTenantSelect = useCallback( async (tenantOption, { refreshOnly = false } = {}) => { const requestedTenantId = tenantOption?.id ?? null; @@ -4697,7 +4708,7 @@ const AppLayout = () => { appDispatch({ type: 'SET_TENANTS', tenants: data.tenants }); } - setWorkspaceMode('table'); + handleDocumentsViewModeChange('list'); navigate('/documents', { replace: true }); await Promise.all([refreshTags(), refreshCorrespondents()]); @@ -4717,7 +4728,7 @@ const AppLayout = () => { notifyApiError, setStatusMessage, resetWorkspaceState, - setWorkspaceMode, + handleDocumentsViewModeChange, navigate, refreshTags, refreshCorrespondents, @@ -4730,7 +4741,6 @@ const AppLayout = () => { currentFolderName, breadcrumbs, onRefresh: refreshCurrentFolder, - onShowSkeuoWorkspace: showSkeuoWorkspace, subfolders: currentSubfolders, documents, searchResults, @@ -4811,7 +4821,6 @@ const AppLayout = () => { selectedDocumentIds, selectedFolderIds, setFocusedRowKey, - showSkeuoWorkspace, tagLookupById, toggleCorrespondentFilter, toggleTagFilter, @@ -4970,13 +4979,15 @@ const AppLayout = () => { ], ); - const skeuoWorkspaceProps = useMemo( + const deskWorkspaceProps = useMemo( () => ({ documents, searchResults, breadcrumbs, currentFolderName, - onExit: exitSkeuoWorkspace, + viewMode: documentsViewMode, + onViewModeChange: handleDocumentsViewModeChange, + onExit: handleDeskExit, onRefresh: refreshCurrentFolder, onDocumentOpen: openDocumentPreview, resolveThumbnailUrl: resolveThumbnailUrlForDoc, @@ -4991,7 +5002,9 @@ const AppLayout = () => { searchResults, breadcrumbs, currentFolderName, - exitSkeuoWorkspace, + documentsViewMode, + handleDocumentsViewModeChange, + handleDeskExit, refreshCurrentFolder, openDocumentPreview, resolveThumbnailUrlForDoc, @@ -5049,10 +5062,8 @@ const AppLayout = () => { handleThumbnailRegeneration, documentsTableProps, detailPanelProps, - workspaceMode, - showSkeuoWorkspace, - exitSkeuoWorkspace, - skeuoWorkspaceProps, + documentsViewMode, + deskWorkspaceProps, ensurePreviewData, ensureAssetUrl, resolveApiPath, @@ -5109,10 +5120,8 @@ const AppLayout = () => { handleThumbnailRegeneration, documentsTableProps, detailPanelProps, - workspaceMode, - showSkeuoWorkspace, - exitSkeuoWorkspace, - skeuoWorkspaceProps, + documentsViewMode, + deskWorkspaceProps, ensurePreviewData, ensureAssetUrl, getDocumentAsset, diff --git a/frontend/src/app/DocumentsRoute.jsx b/frontend/src/app/DocumentsRoute.jsx index 52271b2..5e72ece 100644 --- a/frontend/src/app/DocumentsRoute.jsx +++ b/frontend/src/app/DocumentsRoute.jsx @@ -11,8 +11,8 @@ const DocumentsRoute = () => { documentsTableProps, detailPanelProps, detailPanelOpen, - workspaceMode, - skeuoWorkspaceProps, + documentsViewMode, + deskWorkspaceProps, openTagsModal, openCorrespondentsModal, previewWorkspaceDocument, @@ -65,8 +65,8 @@ const DocumentsRoute = () => { documentsTableProps, detailPanelProps, detailPanelOpen, - workspaceMode, - skeuoWorkspaceProps, + viewMode: documentsViewMode, + deskWorkspaceProps, previewWorkspaceDocument, previewWorkspaceEntry, ensureAssetUrl, diff --git a/frontend/src/app/useWorkspaceSurface.js b/frontend/src/app/useWorkspaceSurface.js index d2259a7..02ba3d0 100644 --- a/frontend/src/app/useWorkspaceSurface.js +++ b/frontend/src/app/useWorkspaceSurface.js @@ -10,8 +10,8 @@ export const useWorkspaceSurface = ({ documentsTableProps, detailPanelProps, detailPanelOpen, - workspaceMode, - skeuoWorkspaceProps, + viewMode, + deskWorkspaceProps, previewWorkspaceDocument, previewWorkspaceEntry, ensureAssetUrl, @@ -62,7 +62,7 @@ export const useWorkspaceSurface = ({ detailPanelOpen, ]); - const showPreviewWorkspace = workspaceMode !== 'skeuo' && Boolean(previewWorkspaceDocument); + const showPreviewWorkspace = viewMode !== 'desk' && Boolean(previewWorkspaceDocument); const previewSurface = useMemo(() => { if (!showPreviewWorkspace || !previewWorkspaceDocument) { @@ -95,24 +95,24 @@ export const useWorkspaceSurface = ({ ]); const workspaceSurface = useMemo(() => { - if (workspaceMode !== 'skeuo') { + if (viewMode !== 'desk') { return null; } return createDesktopSurface({ - workspaceProps: skeuoWorkspaceProps, + workspaceProps: deskWorkspaceProps, renderSidebarToggle, }); - }, [workspaceMode, skeuoWorkspaceProps, renderSidebarToggle]); + }, [viewMode, deskWorkspaceProps, renderSidebarToggle]); const surface = useMemo(() => { if (showPreviewWorkspace) { return previewSurface; } - if (workspaceMode === 'skeuo') { + if (viewMode === 'desk') { return workspaceSurface; } return documentsSurface; - }, [showPreviewWorkspace, workspaceMode, previewSurface, workspaceSurface, documentsSurface]); + }, [showPreviewWorkspace, viewMode, previewSurface, workspaceSurface, documentsSurface]); return { surface }; }; diff --git a/frontend/src/desktop/useDocumentDrag.js b/frontend/src/desktop/useDocumentDrag.js index 97fa8ff..c16f32b 100644 --- a/frontend/src/desktop/useDocumentDrag.js +++ b/frontend/src/desktop/useDocumentDrag.js @@ -37,7 +37,7 @@ const useDocumentDrag = () => { capturedTarget.releasePointerCapture(pointerId); } catch (error) { if (debugDrag) { - console.warn('[skeuo] releasePointerCapture failed', error); + console.warn('[desk] releasePointerCapture failed', error); } } } @@ -52,7 +52,7 @@ const useDocumentDrag = () => { (event, docId) => { if (debugDrag) { console.log( - '[skeuo] handlePointerDown fired for doc', + '[desk] handlePointerDown fired for doc', docId, 'button', event.button, @@ -86,7 +86,7 @@ const useDocumentDrag = () => { capturedTarget.setPointerCapture(event.pointerId); } catch (error) { if (debugDrag) { - console.warn('[skeuo] setPointerCapture failed', error); + console.warn('[desk] setPointerCapture failed', error); } } } @@ -125,14 +125,14 @@ const useDocumentDrag = () => { const state = dragStateRef.current; if (!state) { if (debugDrag) { - console.log('[skeuo] handlePointerMove: no drag state for pointer', event.pointerId); + console.log('[desk] handlePointerMove: no drag state for pointer', event.pointerId); } return; } if (state.pointerId !== event.pointerId) { if (debugDrag) { console.log( - '[skeuo] handlePointerMove: pointer mismatch expected', + '[desk] handlePointerMove: pointer mismatch expected', state.pointerId, 'got', event.pointerId, @@ -143,7 +143,7 @@ const useDocumentDrag = () => { preventAll(event); if (state.locked) { if (debugDrag) { - console.log('[skeuo] handlePointerMove: locked drag for doc', state.docId); + console.log('[desk] handlePointerMove: locked drag for doc', state.docId); } return; } @@ -193,7 +193,7 @@ const useDocumentDrag = () => { ); } if (debugDrag) { - console.log('[skeuo] handlePointerMove: moved doc', state.docId, 'to', clampedCenterX, clampedCenterY); + console.log('[desk] handlePointerMove: moved doc', state.docId, 'to', clampedCenterX, clampedCenterY); } recalcVisibleDocIds(); }, diff --git a/frontend/src/documents/DocumentsTable.jsx b/frontend/src/documents/DocumentsTable.jsx index 7e5ac65..b83c64c 100644 --- a/frontend/src/documents/DocumentsTable.jsx +++ b/frontend/src/documents/DocumentsTable.jsx @@ -7,6 +7,7 @@ import { EditIcon, ViewListIcon, ViewGridIcon, + DesktopIcon, FolderIcon, TrashIcon, RefreshIcon, @@ -197,7 +198,6 @@ const DocumentThumbnailImage = ({ const DocumentsTable = ({ currentFolderName, onRefresh, - onShowSkeuoWorkspace = () => {}, subfolders, documents, searchResults, @@ -272,6 +272,8 @@ const DocumentsTable = ({ } }, []); const isGridView = viewMode === 'grid'; + const isDeskView = viewMode === 'desk'; + const isListView = viewMode === 'list'; const gridIconSize = DEFAULT_GRID_ICON_SIZE; const handleSetViewMode = useCallback( (nextMode) => { @@ -492,9 +494,9 @@ const DocumentsTable = ({
+
-
) : null} @@ -1050,18 +1058,19 @@ export const createDocumentsTableHeaderActions = ({ viewMode, onViewModeChange, onRefresh, - onShowSkeuoWorkspace, }) => { + const isListView = viewMode === 'list'; const isGridView = viewMode === 'grid'; + const isDeskView = viewMode === 'desk'; return ( <>
+