rename skeuo to desktop

This commit is contained in:
2025-10-28 10:41:56 +01:00
parent 94578475ea
commit e150b81190
8 changed files with 1093 additions and 686 deletions
@@ -6,6 +6,20 @@
min-height: 0;
}
.skeuo-item {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
width: auto;
cursor: grab;
touch-action: none;
transform-origin: center center;
transition: box-shadow 0.16s ease;
outline: none;
will-change: transform;
}
.skeuo-shell {
flex: 1;
display: flex;
@@ -33,28 +47,14 @@
font-size: 0.95rem;
}
.skeuo-item {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
width: auto;
cursor: grab;
touch-action: none;
transform-origin: center center;
transition: transform 0.28s ease, box-shadow 0.16s ease;
outline: none;
will-change: transform;
}
.skeuo-item__body {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-grow: 1;
}
.skeuo-item:focus-visible {
@@ -68,8 +68,8 @@
}
.skeuo-item.is-tag-target .skeuo-item__card {
outline: 1em dashed var(--accent);
outline-offset: 1.41em;
outline: 0.35rem dashed var(--accent);
outline-offset: 0.35rem;
}
.skeuo-item.is-tag-pending .skeuo-item__card {
@@ -125,12 +125,12 @@
.skeuo-card__nav {
position: absolute;
bottom: calc(3em * 0.707 * var(--nav-scale));
bottom: 1.8rem;
left: 50%;
transform: translateX(-50%) scale(calc(0.707 * var(--nav-scale, 1)));
transform: translateX(-50%);
transform-origin: center;
display: flex;
gap: 2rem;
gap: 1.5rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
@@ -145,10 +145,10 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: 3.3em;
height: 3.3em;
padding: 0.36em;
border-radius: 3.3em;
width: 2.4em;
height: 2.4em;
padding: 0.25em;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.55);
color: #fff;
@@ -194,9 +194,17 @@ body.skeuo-cursor-remove * {
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 24px 72px rgba(0, 0, 0, 0.24);
width: 100%;
height: 100%;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
overflow: hidden;
--nav-scale: 1;
}
.skeuo-item__card img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.skeuo-item__card--empty {
@@ -238,6 +246,7 @@ body.skeuo-cursor-remove * {
color: #3b3b3b;
max-width: 90%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
}
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
import React, { createContext, useContext } from 'react';
const DesktopContext = createContext(null);
export const DesktopProvider = ({ value, children }) => (
<DesktopContext.Provider value={value}>{children}</DesktopContext.Provider>
);
export const useDesktopContext = () => {
const context = useContext(DesktopContext);
if (!context) {
throw new Error('useDesktopContext must be used within a DesktopProvider');
}
return context;
};
export default DesktopContext;
+17
View File
@@ -0,0 +1,17 @@
export const preventAll = (event) => {
if (!event) {
return;
}
try {
event.preventDefault();
} catch (
// eslint-disable-next-line no-empty
error
) {}
try {
event.stopPropagation();
} catch (
// eslint-disable-next-line no-empty
error
) {}
};
+8
View File
@@ -0,0 +1,8 @@
export const clamp = (value, min, max) => {
if (value < min) return min;
if (value > max) return max;
return value;
};
export const formatTransform = (x, y, rotation = 0, scale = 1) =>
`translate3d(${x}px, ${y}px, 0) rotate(${rotation}deg) scale(${scale})`;
+248
View File
@@ -0,0 +1,248 @@
import { useCallback, useRef } from 'react';
import { useDesktopContext } from './context';
import { preventAll } from './events';
import { clamp, formatTransform } from './math';
const useDocumentDrag = () => {
const {
layoutRef,
itemRefs,
documentLookup,
ensureDocumentSize,
resolveBaseMetrics,
bringToFront,
setDraggingId,
syncLayoutSnapshot,
canvasSize,
openOverlayForDoc,
recalcVisibleDocIds,
settings,
} = useDesktopContext();
const dragStateRef = useRef(null);
const { canvasPadding, defaultCanvasWidth, defaultCanvasHeight, debugDrag } = settings;
const finishDrag = useCallback(
(pointerId) => {
const state = dragStateRef.current;
if (!state || state.pointerId !== pointerId) {
return;
}
const capturedTarget = state.capturedTarget;
if (capturedTarget && typeof capturedTarget.releasePointerCapture === 'function') {
try {
capturedTarget.releasePointerCapture(pointerId);
} catch (
// eslint-disable-next-line no-empty
error
) {}
}
dragStateRef.current = null;
setDraggingId((current) => (current === state.docId ? null : current));
syncLayoutSnapshot();
},
[setDraggingId, syncLayoutSnapshot],
);
const handlePointerDown = useCallback(
(event, docId) => {
if (debugDrag) {
console.log(
'[skeuo] handlePointerDown fired for doc',
docId,
'button',
event.button,
'pointerType',
event.pointerType,
'pointerId',
event.pointerId,
);
}
preventAll(event);
const entry = layoutRef.current.get(docId) || null;
const docKey = docId != null ? String(docId) : null;
const doc = docKey ? documentLookup.get(docKey) : null;
const { width: docWidth, height: docHeight } = ensureDocumentSize(doc);
const { baseScale } = resolveBaseMetrics(doc, docWidth, docHeight);
const normalizedBaseScale =
Number.isFinite(baseScale) && baseScale > 0 ? baseScale : 1;
const defaultCenterX = canvasPadding + docWidth / 2;
const defaultCenterY = canvasPadding + docHeight / 2;
const centerX = typeof entry?.centerX === 'number' ? entry.centerX : defaultCenterX;
const centerY = typeof entry?.centerY === 'number' ? entry.centerY : defaultCenterY;
if (entry && (entry.centerX !== centerX || entry.centerY !== centerY)) {
layoutRef.current.set(docId, { ...entry, centerX, centerY });
}
bringToFront(docId);
const capturedTarget = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
if (capturedTarget && typeof capturedTarget.setPointerCapture === 'function') {
try {
capturedTarget.setPointerCapture(event.pointerId);
} catch (
// eslint-disable-next-line no-empty
error
) {}
}
dragStateRef.current = {
docId,
pointerId: event.pointerId,
originCenterX: centerX,
originCenterY: centerY,
startX: event.clientX,
startY: event.clientY,
rotation: entry?.rotation ?? 0,
moved: false,
locked: false,
width: docWidth,
height: docHeight,
dragScale: 1,
baseScale: normalizedBaseScale,
capturedTarget,
};
setDraggingId(docId);
},
[
bringToFront,
canvasPadding,
documentLookup,
ensureDocumentSize,
layoutRef,
resolveBaseMetrics,
setDraggingId,
debugDrag,
],
);
const handlePointerMove = useCallback(
(event) => {
const state = dragStateRef.current;
if (!state) {
if (debugDrag) {
console.log('[skeuo] handlePointerMove: no drag state for pointer', event.pointerId);
}
return;
}
if (state.pointerId !== event.pointerId) {
if (debugDrag) {
console.log(
'[skeuo] handlePointerMove: pointer mismatch expected',
state.pointerId,
'got',
event.pointerId,
);
}
return;
}
preventAll(event);
if (state.locked) {
if (debugDrag) {
console.log('[skeuo] handlePointerMove: locked drag for doc', state.docId);
}
return;
}
const entry = layoutRef.current.get(state.docId);
if (!entry) {
return;
}
const deltaX = event.clientX - state.startX;
const deltaY = event.clientY - state.startY;
const nextCenterX = state.originCenterX + deltaX;
const nextCenterY = state.originCenterY + deltaY;
const docWidth = state.width;
const docHeight = state.height;
const halfWidth = docWidth / 2;
const halfHeight = docHeight / 2;
const canvasWidth = canvasSize.width || defaultCanvasWidth;
const canvasHeight = canvasSize.height || defaultCanvasHeight;
const minCenterX = canvasPadding + halfWidth;
const maxCenterX = Math.max(minCenterX, canvasWidth - canvasPadding - halfWidth);
const minCenterY = canvasPadding + halfHeight;
const maxCenterY = Math.max(minCenterY, canvasHeight - canvasPadding - halfHeight);
const clampedCenterX = clamp(nextCenterX, minCenterX, maxCenterX);
const clampedCenterY = clamp(nextCenterY, minCenterY, maxCenterY);
const prevCenterX = typeof entry.centerX === 'number' ? entry.centerX : state.originCenterX;
const prevCenterY = typeof entry.centerY === 'number' ? entry.centerY : state.originCenterY;
if (Math.abs(clampedCenterX - prevCenterX) < 0.5 && Math.abs(clampedCenterY - prevCenterY) < 0.5) {
if (debugDrag) {
console.log('[skeuo] handlePointerMove: movement under threshold for doc', state.docId);
}
return;
}
const updated = { ...entry, centerX: clampedCenterX, centerY: clampedCenterY };
layoutRef.current.set(state.docId, updated);
const node = itemRefs.current.get(state.docId);
if (node) {
node.style.transform = formatTransform(
clampedCenterX - state.width / 2,
clampedCenterY - state.height / 2,
state.rotation,
state.dragScale || 1,
);
}
state.moved = true;
if (debugDrag) {
console.log('[skeuo] handlePointerMove: moved doc', state.docId, 'to', clampedCenterX, clampedCenterY);
}
recalcVisibleDocIds();
},
[
canvasPadding,
canvasSize.height,
canvasSize.width,
defaultCanvasHeight,
defaultCanvasWidth,
itemRefs,
layoutRef,
recalcVisibleDocIds,
debugDrag,
],
);
const handlePointerUp = useCallback(
(event) => {
const state = dragStateRef.current;
if (state && state.pointerId === event.pointerId) {
const moved = Boolean(state.moved);
const docId = state.docId;
const shouldOpen = !moved && event.detail >= 2;
finishDrag(event.pointerId);
if (shouldOpen) {
const originInfo = {
rotation: state.rotation || 0,
scale: state.baseScale || 1,
width: state.width,
height: state.height,
};
openOverlayForDoc(docId, originInfo);
}
return;
}
finishDrag(event.pointerId);
},
[finishDrag, openOverlayForDoc],
);
const handlePointerCancel = useCallback(
(event) => {
finishDrag(event.pointerId);
},
[finishDrag],
);
return {
handlePointerDown,
handlePointerMove,
handlePointerUp,
handlePointerCancel,
};
};
export default useDocumentDrag;
+2 -2
View File
@@ -32,7 +32,7 @@ import Sidebar from './sidebar/Sidebar';
import { ChevronsRightIcon } from './ui/icons';
import { createDocumentsSurface } from './documents/DocumentsTable';
import { createPreviewSurface } from './preview/PreviewWorkspace';
import { createSkeuomorphicSurface } from './skeuomorphic_ws';
import { createDesktopSurface } from './DesktopWorkspace';
import { AppShellContext, useAppShell } from './appShellContext';
const runtimeApiBase =
@@ -5526,7 +5526,7 @@ const DocumentsRoute = () => {
if (!isWorkspace) {
return null;
}
return createSkeuomorphicSurface({
return createDesktopSurface({
workspaceProps: skeuoWorkspaceProps,
renderSidebarToggle,
});
+19 -13
View File
@@ -14,11 +14,10 @@
--border: oklch(0.92 0.002 calc(270deg + var(--warmth)));
/* --- Accent (primary) --- */
--accent: oklch(0.61 0.20 calc(260deg + var(--warmth)));
--accent-hover: oklch(0.70 0.18 calc(260deg + var(--warmth)));
--accent: oklch(0.61 0.2 calc(250deg + var(--warmth)));
--accent-hover: oklch(0.70 0.02 calc(100deg + var(--warmth)));
--on-accent: oklch(1 0 0);
/* --- Derived accent states --- */
--accent-soft: color-mix(in oklch, var(--accent) 12%, transparent);
--accent-elevated: color-mix(in oklch, var(--accent) 16%, transparent);
--accent-elevated-strong: color-mix(in oklch, var(--accent) 22%, transparent);
@@ -27,6 +26,12 @@
--accent-focus: color-mix(in oklch, var(--accent) 85%, transparent);
--surface-overlay: color-mix(in oklch, white 82%, transparent);
/* -- Selection --- */
--selection: oklch(0.61 0.01 calc(100deg + var(--warmth)));
/* --- Derived accent states --- */
--selection-soft: color-mix(in oklch, var(--selection) 12%, transparent);
/* --- Shadows & overlays --- */
--shadow-faint: color-mix(in oklch, black 6%, transparent);
--shadow-soft: color-mix(in oklch, black 12%, transparent);
@@ -46,11 +51,12 @@
--danger-subtle: color-mix(in oklch, var(--danger) 8%, transparent);
/* --- Explorer states --- */
--row-hover-bg: color-mix(in oklch, var(--accent) 6%, transparent);
--row-active-bg: color-mix(in oklch, var(--accent) 12%, transparent);
--sidebar-hover-bg: color-mix(in oklch, var(--accent) 8%, transparent);
--sidebar-active-bg: color-mix(in oklch, var(--accent) 16%, transparent);
--row-hover-bg: color-mix(in oklch, var(--selection) 6%, transparent);
--row-active-bg: color-mix(in oklch, var(--selection) 12%, transparent);
--sidebar-hover-bg: color-mix(in oklch, var(--selection) 8%, transparent);
--sidebar-active-bg: color-mix(in oklch, var(--selection) 16%, transparent);
--selection-ring: oklch(0.78 0.16 calc(260deg + var(--warmth)));
--sidebar-active-pill-border: color-mix(in oklch, var(--accent) 64%, transparent);
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 15px;
@@ -1408,7 +1414,7 @@ button.danger:hover:not([disabled]) {
.sidebar-tag-pill:hover {
transform: none;
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.16);
border-color: var(--accent-soft, rgba(59, 130, 246, 0.35));
border-color: var(--accent-soft);
}
.sidebar-tag-pill:focus-visible {
@@ -1417,8 +1423,8 @@ button.danger:hover:not([disabled]) {
}
.sidebar-tag-pill.active {
border-color: var(--accent);
box-shadow: 0 0 0 1.5px var(--accent);
border-color: var(--sidebar-active-pill-border);
box-shadow: 0 0 0 1.5px var(--sidebar-active-pill-border);
}
.sidebar-tag-cloud--has-active .sidebar-tag-pill:not(.active) {
@@ -1458,8 +1464,8 @@ button.danger:hover:not([disabled]) {
}
.sidebar-correspondent-item.active {
background: var(--accent-soft, rgba(59, 130, 246, 0.18));
box-shadow: 0 0 0 1px var(--accent-soft, rgba(59, 130, 246, 0.24));
background: var(--sidebar-active-bg, rgba(59, 130, 246, 0.18));
box-shadow: 0 0 0 1px var(--sidebar-active-bg, rgba(59, 130, 246, 0.24));
color: var(--fg);
}
@@ -1703,7 +1709,7 @@ button.danger:hover:not([disabled]) {
}
.documents-panel tbody tr.document.selected {
background: var(--accent-soft);
background: var(--selection-soft);
box-shadow: inset 2px 0 0 var(--accent-outline-strong);
}