This commit is contained in:
2025-10-31 14:27:30 +01:00
parent b969cae524
commit 91dd3289a9
8 changed files with 169 additions and 151 deletions
+30 -30
View File
@@ -1,12 +1,12 @@
/* Desktop workspace styles */ /* Desktop workspace styles */
.skeuo-main { .desk-main {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0; min-height: 0;
} }
.skeuo-item { .desk-item {
position: absolute; position: absolute;
display: block; display: block;
width: auto; width: auto;
@@ -18,7 +18,7 @@
will-change: transform; will-change: transform;
} }
.skeuo-shell { .desk-shell {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -27,14 +27,14 @@
position: relative; position: relative;
} }
.skeuo-canvas { .desk-canvas {
flex: 1; flex: 1;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
margin: 0; margin: 0;
} }
.skeuo-empty { .desk-empty {
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -47,32 +47,32 @@
.skeuo-item__body { .desk-item__body {
flex-grow: 1; flex-grow: 1;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.skeuo-item:focus-visible { .desk-item:focus-visible {
outline: 2px solid var(--accent); outline: 2px solid var(--accent);
outline-offset: 4px; outline-offset: 4px;
} }
.skeuo-item.is-dragging { .desk-item.is-dragging {
cursor: grabbing; cursor: grabbing;
transition: none; 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: 0.35rem dashed var(--accent);
outline-offset: 0.35rem; outline-offset: 0.35rem;
} }
.skeuo-item.is-tag-pending .skeuo-item__card { .desk-item.is-tag-pending .desk-item__card {
opacity: 0.6; opacity: 0.6;
} }
.skeuo-item.is-filtered-out { .desk-item.is-filtered-out {
opacity: 0.12; opacity: 0.12;
pointer-events: none; pointer-events: none;
filter: blur(15px) grayscale(100%); filter: blur(15px) grayscale(100%);
@@ -80,7 +80,7 @@
z-index: 0 !important; z-index: 0 !important;
} }
.skeuo-item__tags { .desk-item__tags {
--tag-scale: 1; --tag-scale: 1;
position: absolute; position: absolute;
top: 0; top: 0;
@@ -112,14 +112,14 @@
pointer-events: none; pointer-events: none;
} }
.skeuo-item__tags .tag-chip { .desk-item__tags .tag-chip {
font-size: 0.85rem; font-size: 0.85rem;
padding: 0.18rem 0.55rem; padding: 0.18rem 0.55rem;
gap: 0.3rem; gap: 0.3rem;
} }
.skeuo-card__nav { .desk-card__nav {
position: absolute; position: absolute;
bottom: 1.8rem; bottom: 1.8rem;
left: 50%; left: 50%;
@@ -132,12 +132,12 @@
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }
.skeuo-item__card:hover .skeuo-card__nav { .desk-item__card:hover .desk-card__nav {
opacity: 1; opacity: 1;
pointer-events: auto; pointer-events: auto;
} }
.skeuo-card__nav-button { .desk-card__nav-button {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -152,39 +152,39 @@
transition: background 0.15s ease, opacity 0.15s ease; 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); background: var(--preview-nav-bg-hover);
} }
.skeuo-card__nav-button:disabled { .desk-card__nav-button:disabled {
opacity: 0.4; opacity: 0.4;
cursor: default; cursor: default;
} }
.skeuo-card__nav-button:focus-visible { .desk-card__nav-button:focus-visible {
outline: 2px solid var(--accent); outline: 2px solid var(--accent);
outline-offset: 2px; outline-offset: 2px;
} }
.skeuo-card__nav-button svg { .desk-card__nav-button svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.skeuo-item__tags .tag-chip--tear-pending { .desk-item__tags .tag-chip--tear-pending {
opacity: 0.35; opacity: 0.35;
} }
body.skeuo-cursor-remove, body.desk-cursor-remove,
body.skeuo-cursor-remove * { body.desk-cursor-remove * {
cursor: not-allowed !important; cursor: not-allowed !important;
} }
.skeuo-item__shadow { .desk-item__shadow {
display: none; display: none;
} }
.skeuo-item__card { .desk-item__card {
position: relative; position: relative;
border-radius: 0; border-radius: 0;
display: flex; display: flex;
@@ -196,14 +196,14 @@ body.skeuo-cursor-remove * {
overflow: hidden; overflow: hidden;
} }
.skeuo-item__card img { .desk-item__card img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
display: block; display: block;
} }
.skeuo-item__card--empty { .desk-item__card--empty {
box-shadow: 0 12px 32px var(--shadow-medium); box-shadow: 0 12px 32px var(--shadow-medium);
background: 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%), 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; gap: 0.5rem;
} }
.skeuo-item__placeholder { .desk-item__placeholder {
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 500; font-weight: 500;
letter-spacing: normal; letter-spacing: normal;
color: var(--muted); color: var(--muted);
} }
.skeuo-item__empty { .desk-item__empty {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -236,7 +236,7 @@ body.skeuo-cursor-remove * {
text-align: center; text-align: center;
} }
.skeuo-item__title { .desk-item__title {
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 500; font-weight: 500;
color: var(--fg); color: var(--fg);
+59 -63
View File
@@ -8,7 +8,8 @@ import React, {
} from 'react'; } from 'react';
import { resolveDocumentAssetUrl, createAssetView } from './asset_manager'; import { resolveDocumentAssetUrl, createAssetView } from './asset_manager';
import { useAssetNavigator } from './hooks/useAssetNavigator'; 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 { clamp, formatTransform } from './desktop/math';
import { preventAll } from './desktop/events'; import { preventAll } from './desktop/events';
import useDocumentDrag from './desktop/useDocumentDrag'; import useDocumentDrag from './desktop/useDocumentDrag';
@@ -122,7 +123,7 @@ const readTransferData = (dataTransfer, mimeTypes) => {
} }
} catch (error) { } catch (error) {
if (DEBUG_DROP) { 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 { try {
return JSON.parse(raw); return JSON.parse(raw);
} catch (error) { } catch (error) {
console.warn('[skeuo] parseTagTransferPayload failed', error); console.warn('[desk] parseTagTransferPayload failed', error);
} }
return null; return null;
}; };
@@ -207,8 +208,8 @@ const DesktopPreviewCard = ({
onNavigatorSnapshot, onNavigatorSnapshot,
]); ]);
const hasPreview = Boolean(currentUrl); const hasPreview = Boolean(currentUrl);
const cardClasses = ['skeuo-item__card']; const cardClasses = ['desk-item__card'];
if (!hasPreview) cardClasses.push('skeuo-item__card--empty'); if (!hasPreview) cardClasses.push('desk-item__card--empty');
const showNav = hasPreview && (cardinality > 1 || canGoPrev || canGoNext); const showNav = hasPreview && (cardinality > 1 || canGoPrev || canGoNext);
return ( return (
@@ -216,18 +217,18 @@ const DesktopPreviewCard = ({
{hasPreview ? ( {hasPreview ? (
<img src={currentUrl} alt={title} /> <img src={currentUrl} alt={title} />
) : ( ) : (
<div className="skeuo-item__empty"> <div className="desk-item__empty">
<div className="skeuo-item__placeholder">DOC</div> <div className="desk-item__placeholder">DOC</div>
<div className="skeuo-item__title" title={title}> <div className="desk-item__title" title={title}>
{title} {title}
</div> </div>
</div> </div>
)} )}
{showNav ? ( {showNav ? (
<div className="skeuo-card__nav"> <div className="desk-card__nav">
<button <button
type="button" type="button"
className="skeuo-card__nav-button" className="desk-card__nav-button"
onClick={(event) => { onClick={(event) => {
preventAll(event); preventAll(event);
navigator.goPrev(); navigator.goPrev();
@@ -251,7 +252,7 @@ const DesktopPreviewCard = ({
</button> </button>
<button <button
type="button" type="button"
className="skeuo-card__nav-button" className="desk-card__nav-button"
onClick={(event) => { onClick={(event) => {
preventAll(event); preventAll(event);
navigator.goNext(); navigator.goNext();
@@ -463,7 +464,7 @@ const usePreviewMetadata = (documents, getDocumentAsset, ensureAssetUrl) => {
metadata = view.getPrimaryMetadata(); metadata = view.getPrimaryMetadata();
} }
} catch (error) { } catch (error) {
console.warn('[skeuo] ensureDocumentSize metadata fetch failed', error); console.warn('[desk] ensureDocumentSize metadata fetch failed', error);
} }
} }
@@ -750,15 +751,15 @@ const DesktopWorkspace = ({
const focusTarget = () => { const focusTarget = () => {
try { try {
if (DEBUG_FOCUS) { if (DEBUG_FOCUS) {
console.log('[skeuo] focusCanvas -> attempting focus', canvas); console.log('[desk] focusCanvas -> attempting focus', canvas);
} }
canvas.focus({ preventScroll: true }); canvas.focus({ preventScroll: true });
if (DEBUG_FOCUS) { if (DEBUG_FOCUS) {
console.log('[skeuo] focusCanvas: applied focus. activeElement:', document?.activeElement); console.log('[desk] focusCanvas: applied focus. activeElement:', document?.activeElement);
} }
} catch (error) { } catch (error) {
if (DEBUG_FOCUS) { if (DEBUG_FOCUS) {
console.warn('[skeuo] focusTarget failed to focus canvas', error); console.warn('[desk] focusTarget failed to focus canvas', error);
} }
} }
}; };
@@ -769,20 +770,20 @@ const DesktopWorkspace = ({
} }
if (DEBUG_FOCUS) { if (DEBUG_FOCUS) {
console.log('[skeuo] requestCanvasFocus -> scheduling deferred focus'); console.log('[desk] requestCanvasFocus -> scheduling deferred focus');
} }
if (typeof window.requestAnimationFrame === 'function') { if (typeof window.requestAnimationFrame === 'function') {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
if (DEBUG_FOCUS) { if (DEBUG_FOCUS) {
console.log('[skeuo] requestCanvasFocus -> executing deferred focus (rAF)'); console.log('[desk] requestCanvasFocus -> executing deferred focus (rAF)');
} }
focusTarget(); focusTarget();
}); });
} else { } else {
setTimeout(() => { setTimeout(() => {
if (DEBUG_FOCUS) { if (DEBUG_FOCUS) {
console.log('[skeuo] requestCanvasFocus -> executing deferred focus (timeout)'); console.log('[desk] requestCanvasFocus -> executing deferred focus (timeout)');
} }
focusTarget(); focusTarget();
}, 0); }, 0);
@@ -802,9 +803,9 @@ const DesktopWorkspace = ({
} }
removalCursorActiveRef.current = active; removalCursorActiveRef.current = active;
if (active) { if (active) {
body.classList.add('skeuo-cursor-remove'); body.classList.add('desk-cursor-remove');
} else { } else {
body.classList.remove('skeuo-cursor-remove'); body.classList.remove('desk-cursor-remove');
} }
}, []); }, []);
@@ -1080,7 +1081,7 @@ const syncLayoutSnapshot = useCallback(() => {
const nodeEnv = typeof globalThis !== 'undefined' ? globalThis.process?.env?.NODE_ENV : undefined; const nodeEnv = typeof globalThis !== 'undefined' ? globalThis.process?.env?.NODE_ENV : undefined;
if (nodeEnv !== 'production') { if (nodeEnv !== 'production') {
console.log('[skeuo] canvas element', container); console.log('[desk] canvas element', container);
} }
const commitSize = () => { const commitSize = () => {
@@ -1238,7 +1239,7 @@ const syncLayoutSnapshot = useCallback(() => {
return; return;
} }
const container = itemRefs.current.get(docId); const container = itemRefs.current.get(docId);
const imageNode = container?.querySelector?.('.skeuo-item__card img'); const imageNode = container?.querySelector?.('.desk-item__card img');
if (!container || !imageNode) { if (!container || !imageNode) {
return; return;
} }
@@ -1353,24 +1354,24 @@ const syncLayoutSnapshot = useCallback(() => {
async (event, doc) => { async (event, doc) => {
if (!doc || !isTagTransfer(event) || typeof onAssignTagToDocument !== 'function') { if (!doc || !isTagTransfer(event) || typeof onAssignTagToDocument !== 'function') {
if (DEBUG_DROP) { if (DEBUG_DROP) {
console.log('[skeuo] handleTagDropOnDoc: drop ignored', { doc, hasTransfer: isTagTransfer(event) }); console.log('[desk] handleTagDropOnDoc: drop ignored', { doc, hasTransfer: isTagTransfer(event) });
} }
return; return;
} }
preventAll(event); preventAll(event);
if (DEBUG_DROP) { if (DEBUG_DROP) {
console.log('[skeuo] handleTagDropOnDoc: drop accepted for doc', doc.id, 'event', event); console.log('[desk] handleTagDropOnDoc: drop accepted for doc', doc.id, 'event', event);
} }
setTagDropTargetId(null); setTagDropTargetId(null);
const payload = parseTagTransferPayload(event); const payload = parseTagTransferPayload(event);
if (!payload && DEBUG_DROP) { if (!payload && DEBUG_DROP) {
console.warn('[skeuo] handleTagDropOnDoc: failed to parse payload'); console.warn('[desk] handleTagDropOnDoc: failed to parse payload');
} }
if (!payload?.id) { if (!payload?.id) {
if (DEBUG_DROP) { if (DEBUG_DROP) {
console.log('[skeuo] handleTagDropOnDoc: missing tag id payload', payload); console.log('[desk] handleTagDropOnDoc: missing tag id payload', payload);
} }
requestCanvasFocus(); requestCanvasFocus();
return; return;
@@ -1379,13 +1380,13 @@ const syncLayoutSnapshot = useCallback(() => {
const tagId = payload.id; const tagId = payload.id;
const sourceDocId = payload.sourceDocId || null; const sourceDocId = payload.sourceDocId || null;
if (DEBUG_DROP) { if (DEBUG_DROP) {
console.log('[skeuo] handleTagDropOnDoc: parsed payload', { tagId, sourceDocId }); console.log('[desk] handleTagDropOnDoc: parsed payload', { tagId, sourceDocId });
} }
if (sourceDocId && sourceDocId === doc.id) { if (sourceDocId && sourceDocId === doc.id) {
markActiveTagDropHandled(tagId, sourceDocId); markActiveTagDropHandled(tagId, sourceDocId);
if (DEBUG_DROP) { if (DEBUG_DROP) {
console.log('[skeuo] handleTagDropOnDoc: drop from same doc ignored', tagId); console.log('[desk] handleTagDropOnDoc: drop from same doc ignored', tagId);
} }
requestCanvasFocus(); requestCanvasFocus();
return; return;
@@ -1397,7 +1398,7 @@ const syncLayoutSnapshot = useCallback(() => {
if (alreadyAssigned) { if (alreadyAssigned) {
markActiveTagDropHandled(tagId, sourceDocId); markActiveTagDropHandled(tagId, sourceDocId);
if (DEBUG_DROP) { if (DEBUG_DROP) {
console.log('[skeuo] handleTagDropOnDoc: tag already assigned', tagId); console.log('[desk] handleTagDropOnDoc: tag already assigned', tagId);
} }
requestCanvasFocus(); requestCanvasFocus();
return; return;
@@ -1405,7 +1406,7 @@ const syncLayoutSnapshot = useCallback(() => {
const movingBetweenDocuments = Boolean(sourceDocId && sourceDocId !== doc.id); const movingBetweenDocuments = Boolean(sourceDocId && sourceDocId !== doc.id);
if (DEBUG_DROP) { if (DEBUG_DROP) {
console.log('[skeuo] handleTagDropOnDoc: movingBetweenDocuments', movingBetweenDocuments); console.log('[desk] handleTagDropOnDoc: movingBetweenDocuments', movingBetweenDocuments);
} }
setPendingTagDocId(doc.id); setPendingTagDocId(doc.id);
@@ -1413,14 +1414,14 @@ const syncLayoutSnapshot = useCallback(() => {
await onAssignTagToDocument({ documentId: doc.id, tagId, tag: payload }); await onAssignTagToDocument({ documentId: doc.id, tagId, tag: payload });
markActiveTagDropHandled(tagId, sourceDocId); markActiveTagDropHandled(tagId, sourceDocId);
if (DEBUG_DROP) { if (DEBUG_DROP) {
console.log('[skeuo] handleTagDropOnDoc: assigned tag', tagId, 'to doc', doc.id); console.log('[desk] handleTagDropOnDoc: assigned tag', tagId, 'to doc', doc.id);
} }
if (movingBetweenDocuments && typeof onRemoveTagFromDocument === 'function') { if (movingBetweenDocuments && typeof onRemoveTagFromDocument === 'function') {
setPendingRemovalTag({ docId: sourceDocId, tagId }); setPendingRemovalTag({ docId: sourceDocId, tagId });
try { try {
await onRemoveTagFromDocument(sourceDocId, tagId); await onRemoveTagFromDocument(sourceDocId, tagId);
if (DEBUG_DROP) { if (DEBUG_DROP) {
console.log('[skeuo] handleTagDropOnDoc: removed tag from source doc', sourceDocId); console.log('[desk] handleTagDropOnDoc: removed tag from source doc', sourceDocId);
} }
} finally { } finally {
setPendingRemovalTag(null); setPendingRemovalTag(null);
@@ -1429,7 +1430,7 @@ const syncLayoutSnapshot = useCallback(() => {
} finally { } finally {
setPendingTagDocId(null); setPendingTagDocId(null);
if (DEBUG_DROP) { if (DEBUG_DROP) {
console.log('[skeuo] handleTagDropOnDoc: finalizing drop for tag', tagId); console.log('[desk] handleTagDropOnDoc: finalizing drop for tag', tagId);
} }
requestCanvasFocus(); requestCanvasFocus();
} }
@@ -1551,7 +1552,7 @@ const syncLayoutSnapshot = useCallback(() => {
event.dataTransfer.effectAllowed = 'copyMove'; event.dataTransfer.effectAllowed = 'copyMove';
} }
} catch (error) { } catch (error) {
console.warn('[skeuo] Failed to set drag effect', error); console.warn('[desk] Failed to set drag effect', error);
} }
const payload = JSON.stringify({ id: tag.id, label: tag.label, sourceDocId: doc.id }); const payload = JSON.stringify({ id: tag.id, label: tag.label, sourceDocId: doc.id });
@@ -1560,7 +1561,7 @@ const syncLayoutSnapshot = useCallback(() => {
event.dataTransfer?.setData('text/papercrate-tag', payload); event.dataTransfer?.setData('text/papercrate-tag', payload);
event.dataTransfer?.setData('text/plain', tag.label || 'Tag'); event.dataTransfer?.setData('text/plain', tag.label || 'Tag');
} catch (error) { } catch (error) {
console.warn('[skeuo] Failed to populate drag data for tag', error); console.warn('[desk] Failed to populate drag data for tag', error);
} }
const pending = pendingDocTagDragRef.current; const pending = pendingDocTagDragRef.current;
@@ -1591,7 +1592,7 @@ const syncLayoutSnapshot = useCallback(() => {
try { try {
event.dataTransfer.setDragImage(preview.clone, preview.offsetX, preview.offsetY); event.dataTransfer.setDragImage(preview.clone, preview.offsetX, preview.offsetY);
} catch (error) { } catch (error) {
console.warn('[skeuo] Failed to set drag image', error); console.warn('[desk] Failed to set drag image', error);
} }
} }
} }
@@ -1678,7 +1679,7 @@ const syncLayoutSnapshot = useCallback(() => {
}; };
const dropEffect = event?.dataTransfer?.dropEffect || 'none'; const dropEffect = event?.dataTransfer?.dropEffect || 'none';
console.log('[skeuo] dragEnd dropEffect', dropEffect, 'dropHandled', state.dropHandled); console.log('[desk] dragEnd dropEffect', dropEffect, 'dropHandled', state.dropHandled);
const shouldRemove = const shouldRemove =
!state.dropHandled && !state.dropHandled &&
dropEffect === 'none' && dropEffect === 'none' &&
@@ -1687,7 +1688,7 @@ const syncLayoutSnapshot = useCallback(() => {
(state.distance || 0) >= TAG_REMOVE_DISTANCE; (state.distance || 0) >= TAG_REMOVE_DISTANCE;
if (!shouldRemove) { if (!shouldRemove) {
console.log('[skeuo] dragEnd -> no removal. distance:', state.distance); console.log('[desk] dragEnd -> no removal. distance:', state.distance);
scheduleShowNode(); scheduleShowNode();
requestCanvasFocus(); requestCanvasFocus();
updateRemovalCursor(false); updateRemovalCursor(false);
@@ -1700,7 +1701,7 @@ const syncLayoutSnapshot = useCallback(() => {
void (async () => { void (async () => {
try { try {
await onRemoveTagFromDocument(state.sourceDocId, state.tagId); await onRemoveTagFromDocument(state.sourceDocId, state.tagId);
console.log('[skeuo] dragEnd -> removed tag due to fling'); console.log('[desk] dragEnd -> removed tag due to fling');
} catch (error) { } catch (error) {
console.error('Failed to remove tag after drag', error); console.error('Failed to remove tag after drag', error);
scheduleShowNode(); scheduleShowNode();
@@ -1854,20 +1855,20 @@ const DesktopWorkspaceView = () => {
return ( return (
<> <>
<div className="skeuo-shell"> <div className="desk-shell">
<div <div
className="skeuo-canvas" className="desk-canvas"
ref={containerRef} ref={containerRef}
onDragOver={handleCanvasDragOver} onDragOver={handleCanvasDragOver}
onDragLeave={handleCanvasDragLeave} onDragLeave={handleCanvasDragLeave}
onDrop={handleCanvasDrop} onDrop={handleCanvasDrop}
> >
{!allSizesReady ? ( {!allSizesReady ? (
<div className="skeuo-empty"> <div className="desk-empty">
<p>Loading previews</p> <p>Loading previews</p>
</div> </div>
) : items.length === 0 ? ( ) : items.length === 0 ? (
<div className="skeuo-empty"> <div className="desk-empty">
<p>No documents to show here yet. Drop files to make this space come alive.</p> <p>No documents to show here yet. Drop files to make this space come alive.</p>
</div> </div>
) : ( ) : (
@@ -1914,7 +1915,7 @@ const DesktopWorkspaceView = () => {
activeTagSet.size === 0 || docTagKeys.some((key) => activeTagSet.has(key)); activeTagSet.size === 0 || docTagKeys.some((key) => activeTagSet.has(key));
const dropActive = tagDropTargetId === doc.id; const dropActive = tagDropTargetId === doc.id;
const dropPending = pendingTagDocId === doc.id; const dropPending = pendingTagDocId === doc.id;
const itemClasses = ['skeuo-item']; const itemClasses = ['desk-item'];
if (dragging) itemClasses.push('is-dragging'); if (dragging) itemClasses.push('is-dragging');
if (dropActive) itemClasses.push('is-tag-target'); if (dropActive) itemClasses.push('is-tag-target');
if (dropPending) itemClasses.push('is-tag-pending'); if (dropPending) itemClasses.push('is-tag-pending');
@@ -1951,7 +1952,7 @@ const DesktopWorkspaceView = () => {
} }
}} }}
> >
<div className="skeuo-item__body"> <div className="desk-item__body">
<DesktopPreviewCard <DesktopPreviewCard
doc={doc} doc={doc}
title={title} title={title}
@@ -1961,7 +1962,7 @@ const DesktopWorkspaceView = () => {
shouldLoad={shouldLoad} shouldLoad={shouldLoad}
/> />
{tags.length > 0 && ( {tags.length > 0 && (
<div className="skeuo-item__tags" aria-hidden="true"> <div className="desk-item__tags" aria-hidden="true">
{tags.map((tag) => { {tags.map((tag) => {
const key = tag.id || tag.label || String(tag); const key = tag.id || tag.label || String(tag);
if ( if (
@@ -2021,7 +2022,13 @@ export const createDesktopSurface = ({ workspaceProps, renderSidebarToggle }) =>
return null; return null;
} }
const { currentFolderName, searchResults, onRefresh, onExit } = workspaceProps; const {
currentFolderName,
searchResults,
onRefresh,
viewMode,
onViewModeChange,
} = workspaceProps;
const title = Array.isArray(searchResults) ? 'Search results' : currentFolderName; const title = Array.isArray(searchResults) ? 'Search results' : currentFolderName;
const subtitle = Array.isArray(searchResults) const subtitle = Array.isArray(searchResults)
? `${searchResults.length} matching document${searchResults.length === 1 ? '' : 's'}` ? `${searchResults.length} matching document${searchResults.length === 1 ? '' : 's'}`
@@ -2030,22 +2037,11 @@ export const createDesktopSurface = ({ workspaceProps, renderSidebarToggle }) =>
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null; const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
const leading = sidebarToggle ? <>{sidebarToggle}</> : null; const leading = sidebarToggle ? <>{sidebarToggle}</> : null;
const actions = ( const actions = createDocumentsTableHeaderActions({
<> viewMode: viewMode || 'desk',
<button onViewModeChange,
type="button" onRefresh,
className="icon-button" });
onClick={onRefresh}
aria-label="Refresh"
title="Refresh"
>
<RefreshIcon />
</button>
<button type="button" className="secondary" onClick={onExit}>
Back to List
</button>
</>
);
return { return {
key: 'workspace', key: 'workspace',
+31 -22
View File
@@ -190,14 +190,14 @@ const AppLayout = () => {
const [currentFolder, setCurrentFolder] = useState(null); const [currentFolder, setCurrentFolder] = useState(null);
const [currentSubfolders, setCurrentSubfolders] = useState([]); const [currentSubfolders, setCurrentSubfolders] = useState([]);
const [documents, setDocuments] = useState([]); const [documents, setDocuments] = useState([]);
const [workspaceMode, setWorkspaceMode] = useState('table');
const [documentsViewMode, setDocumentsViewMode] = useState(() => { const [documentsViewMode, setDocumentsViewMode] = useState(() => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return 'list'; return 'list';
} }
const stored = window.localStorage.getItem('papercrate_view_mode'); 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 loadThemeSettings = () => {
const defaults = { neutralHue: 260, mode: 'system' }; const defaults = { neutralHue: 260, mode: 'system' };
@@ -258,6 +258,12 @@ const AppLayout = () => {
const initialThemeSettings = loadThemeSettings(); const initialThemeSettings = loadThemeSettings();
const [neutralHue, setNeutralHue] = useState(initialThemeSettings.neutralHue); const [neutralHue, setNeutralHue] = useState(initialThemeSettings.neutralHue);
const [themeMode, setThemeMode] = useState(initialThemeSettings.mode); const [themeMode, setThemeMode] = useState(initialThemeSettings.mode);
useEffect(() => {
if (documentsViewMode !== 'desk') {
lastNonDeskViewRef.current = documentsViewMode;
}
}, [documentsViewMode]);
const initialRowSelection = []; const initialRowSelection = [];
const [selectedRowKeys, setSelectedRowKeys] = useState(initialRowSelection); const [selectedRowKeys, setSelectedRowKeys] = useState(initialRowSelection);
const [selectionOrder, setSelectionOrder] = useState(initialRowSelection); const [selectionOrder, setSelectionOrder] = useState(initialRowSelection);
@@ -281,9 +287,9 @@ const AppLayout = () => {
} }
tagRemovalCursorActiveRef.current = active; tagRemovalCursorActiveRef.current = active;
if (active) { if (active) {
body.classList.add('skeuo-cursor-remove'); body.classList.add('desk-cursor-remove');
} else { } else {
body.classList.remove('skeuo-cursor-remove'); body.classList.remove('desk-cursor-remove');
} }
}, []); }, []);
const refreshAccessToken = useCallback(async () => { const refreshAccessToken = useCallback(async () => {
@@ -4641,11 +4647,9 @@ const AppLayout = () => {
[ensureAssetUrl, getDocumentAsset], [ensureAssetUrl, getDocumentAsset],
); );
const showSkeuoWorkspace = useCallback(() => setWorkspaceMode('skeuo'), [setWorkspaceMode]);
const exitSkeuoWorkspace = useCallback(() => setWorkspaceMode('table'), [setWorkspaceMode]);
const handleDocumentsViewModeChange = useCallback((mode) => { const handleDocumentsViewModeChange = useCallback((mode) => {
const next = mode === 'grid' ? 'grid' : mode === 'desk' ? 'desk' : 'list';
setDocumentsViewMode((previous) => { setDocumentsViewMode((previous) => {
const next = mode === 'grid' ? 'grid' : 'list';
if (next !== previous && typeof window !== 'undefined') { if (next !== previous && typeof window !== 'undefined') {
window.localStorage.setItem('papercrate_view_mode', next); 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( const handleTenantSelect = useCallback(
async (tenantOption, { refreshOnly = false } = {}) => { async (tenantOption, { refreshOnly = false } = {}) => {
const requestedTenantId = tenantOption?.id ?? null; const requestedTenantId = tenantOption?.id ?? null;
@@ -4697,7 +4708,7 @@ const AppLayout = () => {
appDispatch({ type: 'SET_TENANTS', tenants: data.tenants }); appDispatch({ type: 'SET_TENANTS', tenants: data.tenants });
} }
setWorkspaceMode('table'); handleDocumentsViewModeChange('list');
navigate('/documents', { replace: true }); navigate('/documents', { replace: true });
await Promise.all([refreshTags(), refreshCorrespondents()]); await Promise.all([refreshTags(), refreshCorrespondents()]);
@@ -4717,7 +4728,7 @@ const AppLayout = () => {
notifyApiError, notifyApiError,
setStatusMessage, setStatusMessage,
resetWorkspaceState, resetWorkspaceState,
setWorkspaceMode, handleDocumentsViewModeChange,
navigate, navigate,
refreshTags, refreshTags,
refreshCorrespondents, refreshCorrespondents,
@@ -4730,7 +4741,6 @@ const AppLayout = () => {
currentFolderName, currentFolderName,
breadcrumbs, breadcrumbs,
onRefresh: refreshCurrentFolder, onRefresh: refreshCurrentFolder,
onShowSkeuoWorkspace: showSkeuoWorkspace,
subfolders: currentSubfolders, subfolders: currentSubfolders,
documents, documents,
searchResults, searchResults,
@@ -4811,7 +4821,6 @@ const AppLayout = () => {
selectedDocumentIds, selectedDocumentIds,
selectedFolderIds, selectedFolderIds,
setFocusedRowKey, setFocusedRowKey,
showSkeuoWorkspace,
tagLookupById, tagLookupById,
toggleCorrespondentFilter, toggleCorrespondentFilter,
toggleTagFilter, toggleTagFilter,
@@ -4970,13 +4979,15 @@ const AppLayout = () => {
], ],
); );
const skeuoWorkspaceProps = useMemo( const deskWorkspaceProps = useMemo(
() => ({ () => ({
documents, documents,
searchResults, searchResults,
breadcrumbs, breadcrumbs,
currentFolderName, currentFolderName,
onExit: exitSkeuoWorkspace, viewMode: documentsViewMode,
onViewModeChange: handleDocumentsViewModeChange,
onExit: handleDeskExit,
onRefresh: refreshCurrentFolder, onRefresh: refreshCurrentFolder,
onDocumentOpen: openDocumentPreview, onDocumentOpen: openDocumentPreview,
resolveThumbnailUrl: resolveThumbnailUrlForDoc, resolveThumbnailUrl: resolveThumbnailUrlForDoc,
@@ -4991,7 +5002,9 @@ const AppLayout = () => {
searchResults, searchResults,
breadcrumbs, breadcrumbs,
currentFolderName, currentFolderName,
exitSkeuoWorkspace, documentsViewMode,
handleDocumentsViewModeChange,
handleDeskExit,
refreshCurrentFolder, refreshCurrentFolder,
openDocumentPreview, openDocumentPreview,
resolveThumbnailUrlForDoc, resolveThumbnailUrlForDoc,
@@ -5049,10 +5062,8 @@ const AppLayout = () => {
handleThumbnailRegeneration, handleThumbnailRegeneration,
documentsTableProps, documentsTableProps,
detailPanelProps, detailPanelProps,
workspaceMode, documentsViewMode,
showSkeuoWorkspace, deskWorkspaceProps,
exitSkeuoWorkspace,
skeuoWorkspaceProps,
ensurePreviewData, ensurePreviewData,
ensureAssetUrl, ensureAssetUrl,
resolveApiPath, resolveApiPath,
@@ -5109,10 +5120,8 @@ const AppLayout = () => {
handleThumbnailRegeneration, handleThumbnailRegeneration,
documentsTableProps, documentsTableProps,
detailPanelProps, detailPanelProps,
workspaceMode, documentsViewMode,
showSkeuoWorkspace, deskWorkspaceProps,
exitSkeuoWorkspace,
skeuoWorkspaceProps,
ensurePreviewData, ensurePreviewData,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
+4 -4
View File
@@ -11,8 +11,8 @@ const DocumentsRoute = () => {
documentsTableProps, documentsTableProps,
detailPanelProps, detailPanelProps,
detailPanelOpen, detailPanelOpen,
workspaceMode, documentsViewMode,
skeuoWorkspaceProps, deskWorkspaceProps,
openTagsModal, openTagsModal,
openCorrespondentsModal, openCorrespondentsModal,
previewWorkspaceDocument, previewWorkspaceDocument,
@@ -65,8 +65,8 @@ const DocumentsRoute = () => {
documentsTableProps, documentsTableProps,
detailPanelProps, detailPanelProps,
detailPanelOpen, detailPanelOpen,
workspaceMode, viewMode: documentsViewMode,
skeuoWorkspaceProps, deskWorkspaceProps,
previewWorkspaceDocument, previewWorkspaceDocument,
previewWorkspaceEntry, previewWorkspaceEntry,
ensureAssetUrl, ensureAssetUrl,
+8 -8
View File
@@ -10,8 +10,8 @@ export const useWorkspaceSurface = ({
documentsTableProps, documentsTableProps,
detailPanelProps, detailPanelProps,
detailPanelOpen, detailPanelOpen,
workspaceMode, viewMode,
skeuoWorkspaceProps, deskWorkspaceProps,
previewWorkspaceDocument, previewWorkspaceDocument,
previewWorkspaceEntry, previewWorkspaceEntry,
ensureAssetUrl, ensureAssetUrl,
@@ -62,7 +62,7 @@ export const useWorkspaceSurface = ({
detailPanelOpen, detailPanelOpen,
]); ]);
const showPreviewWorkspace = workspaceMode !== 'skeuo' && Boolean(previewWorkspaceDocument); const showPreviewWorkspace = viewMode !== 'desk' && Boolean(previewWorkspaceDocument);
const previewSurface = useMemo(() => { const previewSurface = useMemo(() => {
if (!showPreviewWorkspace || !previewWorkspaceDocument) { if (!showPreviewWorkspace || !previewWorkspaceDocument) {
@@ -95,24 +95,24 @@ export const useWorkspaceSurface = ({
]); ]);
const workspaceSurface = useMemo(() => { const workspaceSurface = useMemo(() => {
if (workspaceMode !== 'skeuo') { if (viewMode !== 'desk') {
return null; return null;
} }
return createDesktopSurface({ return createDesktopSurface({
workspaceProps: skeuoWorkspaceProps, workspaceProps: deskWorkspaceProps,
renderSidebarToggle, renderSidebarToggle,
}); });
}, [workspaceMode, skeuoWorkspaceProps, renderSidebarToggle]); }, [viewMode, deskWorkspaceProps, renderSidebarToggle]);
const surface = useMemo(() => { const surface = useMemo(() => {
if (showPreviewWorkspace) { if (showPreviewWorkspace) {
return previewSurface; return previewSurface;
} }
if (workspaceMode === 'skeuo') { if (viewMode === 'desk') {
return workspaceSurface; return workspaceSurface;
} }
return documentsSurface; return documentsSurface;
}, [showPreviewWorkspace, workspaceMode, previewSurface, workspaceSurface, documentsSurface]); }, [showPreviewWorkspace, viewMode, previewSurface, workspaceSurface, documentsSurface]);
return { surface }; return { surface };
}; };
+7 -7
View File
@@ -37,7 +37,7 @@ const useDocumentDrag = () => {
capturedTarget.releasePointerCapture(pointerId); capturedTarget.releasePointerCapture(pointerId);
} catch (error) { } catch (error) {
if (debugDrag) { if (debugDrag) {
console.warn('[skeuo] releasePointerCapture failed', error); console.warn('[desk] releasePointerCapture failed', error);
} }
} }
} }
@@ -52,7 +52,7 @@ const useDocumentDrag = () => {
(event, docId) => { (event, docId) => {
if (debugDrag) { if (debugDrag) {
console.log( console.log(
'[skeuo] handlePointerDown fired for doc', '[desk] handlePointerDown fired for doc',
docId, docId,
'button', 'button',
event.button, event.button,
@@ -86,7 +86,7 @@ const useDocumentDrag = () => {
capturedTarget.setPointerCapture(event.pointerId); capturedTarget.setPointerCapture(event.pointerId);
} catch (error) { } catch (error) {
if (debugDrag) { if (debugDrag) {
console.warn('[skeuo] setPointerCapture failed', error); console.warn('[desk] setPointerCapture failed', error);
} }
} }
} }
@@ -125,14 +125,14 @@ const useDocumentDrag = () => {
const state = dragStateRef.current; const state = dragStateRef.current;
if (!state) { if (!state) {
if (debugDrag) { 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; return;
} }
if (state.pointerId !== event.pointerId) { if (state.pointerId !== event.pointerId) {
if (debugDrag) { if (debugDrag) {
console.log( console.log(
'[skeuo] handlePointerMove: pointer mismatch expected', '[desk] handlePointerMove: pointer mismatch expected',
state.pointerId, state.pointerId,
'got', 'got',
event.pointerId, event.pointerId,
@@ -143,7 +143,7 @@ const useDocumentDrag = () => {
preventAll(event); preventAll(event);
if (state.locked) { if (state.locked) {
if (debugDrag) { if (debugDrag) {
console.log('[skeuo] handlePointerMove: locked drag for doc', state.docId); console.log('[desk] handlePointerMove: locked drag for doc', state.docId);
} }
return; return;
} }
@@ -193,7 +193,7 @@ const useDocumentDrag = () => {
); );
} }
if (debugDrag) { 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(); recalcVisibleDocIds();
}, },
+27 -14
View File
@@ -7,6 +7,7 @@ import {
EditIcon, EditIcon,
ViewListIcon, ViewListIcon,
ViewGridIcon, ViewGridIcon,
DesktopIcon,
FolderIcon, FolderIcon,
TrashIcon, TrashIcon,
RefreshIcon, RefreshIcon,
@@ -197,7 +198,6 @@ const DocumentThumbnailImage = ({
const DocumentsTable = ({ const DocumentsTable = ({
currentFolderName, currentFolderName,
onRefresh, onRefresh,
onShowSkeuoWorkspace = () => {},
subfolders, subfolders,
documents, documents,
searchResults, searchResults,
@@ -272,6 +272,8 @@ const DocumentsTable = ({
} }
}, []); }, []);
const isGridView = viewMode === 'grid'; const isGridView = viewMode === 'grid';
const isDeskView = viewMode === 'desk';
const isListView = viewMode === 'list';
const gridIconSize = DEFAULT_GRID_ICON_SIZE; const gridIconSize = DEFAULT_GRID_ICON_SIZE;
const handleSetViewMode = useCallback( const handleSetViewMode = useCallback(
(nextMode) => { (nextMode) => {
@@ -492,9 +494,9 @@ const DocumentsTable = ({
<div className="view-toggle" role="group" aria-label="Change view"> <div className="view-toggle" role="group" aria-label="Change view">
<button <button
type="button" type="button"
className={`view-toggle__button${isGridView ? '' : ' active'}`} className={`view-toggle__button${isListView ? ' active' : ''}`}
onClick={() => handleSetViewMode('list')} onClick={() => handleSetViewMode('list')}
aria-pressed={!isGridView} aria-pressed={isListView}
title="List view" title="List view"
> >
<ViewListIcon className="view-toggle__icon" size={18} /> <ViewListIcon className="view-toggle__icon" size={18} />
@@ -508,13 +510,19 @@ const DocumentsTable = ({
> >
<ViewGridIcon className="view-toggle__icon" size={18} /> <ViewGridIcon className="view-toggle__icon" size={18} />
</button> </button>
<button
type="button"
className={`view-toggle__button${isDeskView ? ' active' : ''}`}
onClick={() => handleSetViewMode('desk')}
aria-pressed={isDeskView}
title="Desk view"
>
<DesktopIcon className="view-toggle__icon" size={18} />
</button>
</div> </div>
<button className="secondary" onClick={onRefresh}> <button className="secondary" onClick={onRefresh}>
Refresh Refresh
</button> </button>
<button className="secondary" type="button" onClick={onShowSkeuoWorkspace}>
Desk View
</button>
</div> </div>
</div> </div>
) : null} ) : null}
@@ -1050,18 +1058,19 @@ export const createDocumentsTableHeaderActions = ({
viewMode, viewMode,
onViewModeChange, onViewModeChange,
onRefresh, onRefresh,
onShowSkeuoWorkspace,
}) => { }) => {
const isListView = viewMode === 'list';
const isGridView = viewMode === 'grid'; const isGridView = viewMode === 'grid';
const isDeskView = viewMode === 'desk';
return ( return (
<> <>
<div className="view-toggle" role="group" aria-label="Change view"> <div className="view-toggle" role="group" aria-label="Change view">
<button <button
type="button" type="button"
className={`view-toggle__button${isGridView ? '' : ' active'}`} className={`view-toggle__button${isListView ? ' active' : ''}`}
onClick={() => onViewModeChange?.('list')} onClick={() => onViewModeChange?.('list')}
aria-pressed={!isGridView} aria-pressed={isListView}
title="List view" title="List view"
> >
<ViewListIcon className="view-toggle__icon" size={18} /> <ViewListIcon className="view-toggle__icon" size={18} />
@@ -1075,6 +1084,15 @@ export const createDocumentsTableHeaderActions = ({
> >
<ViewGridIcon className="view-toggle__icon" size={18} /> <ViewGridIcon className="view-toggle__icon" size={18} />
</button> </button>
<button
type="button"
className={`view-toggle__button${isDeskView ? ' active' : ''}`}
onClick={() => onViewModeChange?.('desk')}
aria-pressed={isDeskView}
title="Desk view"
>
<DesktopIcon className="view-toggle__icon" size={18} />
</button>
</div> </div>
<span className="main-content__actions-divider" aria-hidden="true"> <span className="main-content__actions-divider" aria-hidden="true">
<MinusVerticalIcon /> <MinusVerticalIcon />
@@ -1088,9 +1106,6 @@ export const createDocumentsTableHeaderActions = ({
> >
<RefreshIcon /> <RefreshIcon />
</button> </button>
<button className="secondary" type="button" onClick={onShowSkeuoWorkspace}>
Desk View
</button>
</> </>
); );
}; };
@@ -1109,7 +1124,6 @@ export const createDocumentsSurface = ({
viewMode, viewMode,
onViewModeChange, onViewModeChange,
onRefresh, onRefresh,
onShowSkeuoWorkspace,
} = tableProps; } = tableProps;
const title = Array.isArray(searchResults) ? 'Search results' : currentFolderName; const title = Array.isArray(searchResults) ? 'Search results' : currentFolderName;
@@ -1121,7 +1135,6 @@ export const createDocumentsSurface = ({
viewMode, viewMode,
onViewModeChange, onViewModeChange,
onRefresh, onRefresh,
onShowSkeuoWorkspace,
}); });
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null; const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
+2 -2
View File
@@ -2571,11 +2571,11 @@ button.danger:hover:not([disabled]) {
} }
.preview-pane--stack:hover .preview-pane__nav--overlay, .preview-pane--stack:hover .preview-pane__nav--overlay,
.skeuo-item__card:hover .preview-pane__nav--overlay { .desk-item__card:hover .preview-pane__nav--overlay {
opacity: 1; opacity: 1;
} }
.skeuo-card__nav { .desk-card__nav {
bottom: 0.5rem; bottom: 0.5rem;
} }