refactor: introduce dedicated identifier types for improved clarity and type safety
This commit is contained in:
@@ -3,17 +3,18 @@ import DesktopPreviewCard from './DesktopPreviewCard';
|
||||
import { resolveCorrespondents } from '../documents/correspondents';
|
||||
import { getTagColorStyle } from '../utils/colors';
|
||||
import { preventAll } from './events';
|
||||
import type { DocumentId } from '../types/identifiers';
|
||||
|
||||
type DocumentLike = {
|
||||
id?: string | number;
|
||||
id?: string;
|
||||
title?: string;
|
||||
tags?: Array<{ id?: string | number; label?: string; color?: string | null }>;
|
||||
tags?: Array<{ id?: string; label?: string; color?: string | null }>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
interface PendingRemovalTag {
|
||||
docId?: string | number;
|
||||
tagId?: string | number;
|
||||
docId?: string;
|
||||
tagId?: string;
|
||||
}
|
||||
|
||||
interface DesktopDocumentCardProps {
|
||||
@@ -30,10 +31,10 @@ interface DesktopDocumentCardProps {
|
||||
getDocumentAsset?: (...args: any[]) => unknown;
|
||||
handleNavigatorSnapshot?: (...args: any[]) => void;
|
||||
cardPointerHandlers?: React.HTMLAttributes<HTMLDivElement>;
|
||||
onDocumentActivate?: (id: string | number) => void;
|
||||
onTagDragEnter?: (event: React.DragEvent<HTMLDivElement>, docId: string | number) => void;
|
||||
onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, docId: string | number) => void;
|
||||
onTagDragLeave?: (event: React.DragEvent<HTMLDivElement>, docId: string | number) => void;
|
||||
onDocumentActivate?: (id: string) => void;
|
||||
onTagDragEnter?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
|
||||
onTagDragOver?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
|
||||
onTagDragLeave?: (event: React.DragEvent<HTMLDivElement>, docId: DocumentId) => void;
|
||||
onTagDrop?: (event: React.DragEvent<HTMLDivElement>, doc: DocumentLike) => void;
|
||||
onDocTagPointerDown?: (event: React.PointerEvent<HTMLElement>, doc: DocumentLike, tag: any) => void;
|
||||
onDocTagDragStart?: (event: React.DragEvent<HTMLElement>, doc: DocumentLike, tag: any) => void;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
import { useAssetNavigator } from '../hooks/useAssetNavigator';
|
||||
|
||||
type Identifier = string | number;
|
||||
import type { Identifier } from '../types/identifiers';
|
||||
|
||||
interface DocumentLike {
|
||||
id?: Identifier;
|
||||
@@ -21,7 +20,7 @@ interface AssetLike {
|
||||
type EnsureAssetUrl = (
|
||||
documentId: Identifier,
|
||||
asset: AssetLike,
|
||||
options?: { force?: boolean; [key: string]: unknown },
|
||||
options?: { force?: boolean;[key: string]: unknown },
|
||||
) => Promise<unknown>;
|
||||
|
||||
type GetDocumentAsset = (document: DocumentLike | null, assetType: string) => AssetLike | null;
|
||||
|
||||
@@ -29,8 +29,7 @@ import '../styles/workspace/workspace-layout.css';
|
||||
import '../styles/workspace/workspace-items.css';
|
||||
import '../styles/workspace/workspace-cards.css';
|
||||
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
|
||||
|
||||
type Identifier = string | number;
|
||||
import type { DocumentId, Identifier } from '../types/identifiers';
|
||||
|
||||
type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null;
|
||||
type DocumentLinkLike = { url?: string | null; mimeType?: string | null };
|
||||
@@ -79,7 +78,7 @@ interface DocumentSizeInfo {
|
||||
}
|
||||
|
||||
interface PreviewMetadataEntry {
|
||||
docId: string;
|
||||
docId: DocumentId;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
@@ -225,7 +224,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||
const syntheticEvent = event || ({
|
||||
metaKey: true,
|
||||
ctrlKey: true,
|
||||
preventDefault: () => {},
|
||||
preventDefault: () => { },
|
||||
} as unknown as PointerEvent);
|
||||
docIds.forEach((id) => {
|
||||
const key = getDocRowKey(id);
|
||||
@@ -354,7 +353,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||
engine.recalcVisibleDocIds();
|
||||
}, [engine]);
|
||||
|
||||
const setDraggingId = useCallback((value: string | number | null) => {
|
||||
const setDraggingId = useCallback((value: string | null) => {
|
||||
engine.setDraggingId(value);
|
||||
}, [engine]);
|
||||
|
||||
@@ -686,7 +685,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||
},
|
||||
[resolvePreviewDimensions],
|
||||
);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (draggingId && !items.some((doc) => String(doc.id) === draggingId)) {
|
||||
@@ -1072,8 +1071,8 @@ function DesktopWorkspaceView({
|
||||
const dragging = docKey ? draggingId === docKey : false;
|
||||
const docTagKeys = Array.isArray(doc?.tags)
|
||||
? doc.tags
|
||||
.map((tag) => (tag?.id != null ? String(tag.id) : null))
|
||||
.filter((id): id is string => Boolean(id))
|
||||
.map((tag) => (tag?.id != null ? String(tag.id) : null))
|
||||
.filter((id): id is string => Boolean(id))
|
||||
: [];
|
||||
const matchesFilter =
|
||||
activeTagSet.size === 0 || docTagKeys.some((key) => activeTagSet.has(key));
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { DocumentId } from '../types/identifiers';
|
||||
type TenantId = import('../types/identifiers').TenantId;
|
||||
|
||||
const DB_NAME = 'papercrate_desk';
|
||||
const DB_VERSION = 1;
|
||||
const LAYOUT_STORE = 'layouts';
|
||||
@@ -107,9 +110,9 @@ const withStore = async <T>(mode: TransactionMode, handler: (store: IDBObjectSto
|
||||
};
|
||||
|
||||
interface LayoutRecord {
|
||||
tenantId: string | number;
|
||||
viewId: string | number;
|
||||
documentId: string | number;
|
||||
tenantId: TenantId;
|
||||
viewId: string;
|
||||
documentId: DocumentId;
|
||||
centerX?: number;
|
||||
centerY?: number;
|
||||
rotation?: number;
|
||||
@@ -117,7 +120,13 @@ interface LayoutRecord {
|
||||
updatedAt?: number;
|
||||
}
|
||||
|
||||
export const fetchLayoutRecords = async ({ tenantId, viewId }: { tenantId?: string | number; viewId?: string | number }): Promise<LayoutRecord[]> => {
|
||||
export const fetchLayoutRecords = async ({
|
||||
tenantId,
|
||||
viewId,
|
||||
}: {
|
||||
tenantId?: TenantId;
|
||||
viewId?: string;
|
||||
}): Promise<LayoutRecord[]> => {
|
||||
if (!tenantId || !viewId) {
|
||||
return [];
|
||||
}
|
||||
@@ -133,7 +142,22 @@ export const fetchLayoutRecords = async ({ tenantId, viewId }: { tenantId?: stri
|
||||
}
|
||||
};
|
||||
|
||||
export const upsertLayoutRecords = async ({ tenantId, viewId, entries }: { tenantId?: string | number; viewId?: string | number; entries?: Array<{ documentId?: string | number; centerX?: number; centerY?: number; rotation?: number; zIndex?: number; updatedAt?: number }> }) => {
|
||||
export const upsertLayoutRecords = async ({
|
||||
tenantId,
|
||||
viewId,
|
||||
entries,
|
||||
}: {
|
||||
tenantId?: TenantId;
|
||||
viewId?: string;
|
||||
entries?: Array<{
|
||||
documentId?: DocumentId;
|
||||
centerX?: number;
|
||||
centerY?: number;
|
||||
rotation?: number;
|
||||
zIndex?: number;
|
||||
updatedAt?: number;
|
||||
}>;
|
||||
}) => {
|
||||
if (!tenantId || !viewId || !Array.isArray(entries) || !entries.length) {
|
||||
return;
|
||||
}
|
||||
@@ -162,7 +186,7 @@ export const upsertLayoutRecords = async ({ tenantId, viewId, entries }: { tenan
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTenantLayouts = async (tenantId?: string | number) => {
|
||||
export const deleteTenantLayouts = async (tenantId?: TenantId) => {
|
||||
if (!tenantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { DocumentId } from '../../types/identifiers';
|
||||
interface DocumentLike {
|
||||
id?: string | number;
|
||||
id?: string;
|
||||
current_version?: unknown;
|
||||
tags?: unknown;
|
||||
}
|
||||
|
||||
interface AssetLike {
|
||||
id?: string | number;
|
||||
id?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface PreviewMetadataEntry {
|
||||
docId: string;
|
||||
docId: DocumentId;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
type GetDocumentAsset = (doc: DocumentLike, type: string) => AssetLike | null;
|
||||
type EnsureAssetUrl = (docId: string | number, asset: AssetLike, options?: { force?: boolean }) => Promise<AssetLike | null>;
|
||||
type EnsureAssetUrl = (docId: DocumentId, asset: AssetLike, options?: { force?: boolean }) => Promise<AssetLike | null>;
|
||||
|
||||
const usePreviewMetadata = (
|
||||
documents: DocumentLike[] | null,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { safeInvoke } from '../events';
|
||||
import type { DocumentId } from '../../types/identifiers';
|
||||
|
||||
export const CLICK_ACTIONS = {
|
||||
selectSingle: 'selectSingle',
|
||||
@@ -25,9 +26,9 @@ export const LONG_PRESS_DURATION_MS = 450;
|
||||
export const withinThreshold = (dx: number, dy: number, thresholdSquared: number): boolean => (dx * dx + dy * dy) <= thresholdSquared;
|
||||
|
||||
interface PointerIntentArgs {
|
||||
doc: { id: string | number };
|
||||
doc: { id: string };
|
||||
entryDescriptor: unknown;
|
||||
selectedDocumentIds: Array<string | number>;
|
||||
selectedDocumentIds: Array<string>;
|
||||
metaKey: boolean;
|
||||
pointerButton?: number;
|
||||
pointerType?: string;
|
||||
@@ -35,7 +36,7 @@ interface PointerIntentArgs {
|
||||
}
|
||||
|
||||
export interface PointerIntent {
|
||||
docId: string | number;
|
||||
docId: DocumentId;
|
||||
entryDescriptor: unknown;
|
||||
pointerType?: string;
|
||||
pointerButton?: number;
|
||||
|
||||
@@ -19,8 +19,7 @@ import {
|
||||
applyDomTransform,
|
||||
type WorkspaceEngine,
|
||||
} from './workspaceEngine';
|
||||
|
||||
type Identifier = string | number;
|
||||
import type { DocumentId, Identifier } from '../types/identifiers';
|
||||
|
||||
interface DocumentLike {
|
||||
id?: Identifier | null;
|
||||
@@ -121,7 +120,7 @@ interface UseDocumentDragOptions {
|
||||
type EngineInertiaState = Parameters<WorkspaceEngine['startInertiaAnimation']>[1];
|
||||
|
||||
interface DragStateInternal extends EngineDragState {
|
||||
docId: string;
|
||||
docId: DocumentId;
|
||||
docKey: string;
|
||||
pointerId: number;
|
||||
originCenterX: number;
|
||||
@@ -208,15 +207,15 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
bringToFront,
|
||||
setDraggingId,
|
||||
canvasSize,
|
||||
openOverlayForDoc,
|
||||
recalcVisibleDocIds,
|
||||
settings,
|
||||
containerRef: providedContainerRef,
|
||||
onDocumentActivate,
|
||||
onDocumentStackSelect,
|
||||
selectedDocumentIds = [],
|
||||
markLayoutDirty,
|
||||
} = options;
|
||||
openOverlayForDoc,
|
||||
recalcVisibleDocIds,
|
||||
settings,
|
||||
containerRef: providedContainerRef,
|
||||
onDocumentActivate,
|
||||
onDocumentStackSelect,
|
||||
selectedDocumentIds = [],
|
||||
markLayoutDirty,
|
||||
} = options;
|
||||
|
||||
const fallbackContainerRef = useRef<HTMLElement | null>(null);
|
||||
const containerRef = providedContainerRef ?? fallbackContainerRef;
|
||||
@@ -237,7 +236,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
|
||||
const tapHandler = usePointerTap<DragTapMetadata>({
|
||||
delay: 220,
|
||||
onSingle: () => {},
|
||||
onSingle: () => { },
|
||||
onDouble: ({ data, event }) => {
|
||||
if (!data?.docId) {
|
||||
return;
|
||||
@@ -281,8 +280,8 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
}
|
||||
const keys = Array.isArray(docIds) && docIds.length
|
||||
? docIds
|
||||
.map((id) => (id != null ? String(id) : null))
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.map((id) => (id != null ? String(id) : null))
|
||||
.filter((value): value is string => Boolean(value))
|
||||
: Array.from(map.keys());
|
||||
keys.forEach((key) => {
|
||||
const transform = map.get(key);
|
||||
@@ -350,8 +349,8 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
const stackDocIdsOptionRaw = options?.stackDocIds;
|
||||
const stackDocIdsOption = Array.isArray(stackDocIdsOptionRaw)
|
||||
? stackDocIdsOptionRaw
|
||||
.map((value) => (value != null ? String(value) : null))
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.map((value) => (value != null ? String(value) : null))
|
||||
.filter((value): value is string => Boolean(value))
|
||||
: null;
|
||||
const stackSelectionAppliedInitial = Boolean(options?.stackSelectionApplied);
|
||||
const wasSelectedAtPointerDown = Boolean(options?.wasSelected);
|
||||
@@ -361,8 +360,8 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
|
||||
let selectionIds: string[] = Array.isArray(selectedDocumentIds)
|
||||
? selectedDocumentIds
|
||||
.map((id) => (id != null ? String(id) : null))
|
||||
.filter((id): id is string => Boolean(id))
|
||||
.map((id) => (id != null ? String(id) : null))
|
||||
.filter((id): id is string => Boolean(id))
|
||||
: [];
|
||||
|
||||
if (!stackDocIdsOption && !pointerModifierActive && !wasSelectedAtPointerDown) {
|
||||
@@ -399,7 +398,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
if (isGroupDrag) {
|
||||
selectionIds.forEach((id) => {
|
||||
if (id !== docKey) {
|
||||
engine?.cancelInertiaAnimation?.(id);
|
||||
engine?.cancelInertiaAnimation?.(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -491,7 +490,7 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
offsetY: baseOffsetY,
|
||||
targetRotation: initialRotation,
|
||||
displayRotation: initialRotation,
|
||||
|
||||
|
||||
} satisfies DragGroupItemInternal;
|
||||
});
|
||||
|
||||
@@ -569,28 +568,28 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
|
||||
setDraggingId(docKey);
|
||||
|
||||
if (isGroupDrag) {
|
||||
groupItems.forEach((item) => {
|
||||
if (item.docId === docKey) {
|
||||
return;
|
||||
}
|
||||
const node = itemRefs.current.get(item.docId);
|
||||
if (node) {
|
||||
item.displayRotation = item.initialRotation;
|
||||
const itemEntry = layoutRef.current.get(item.docId) || null;
|
||||
applyDomTransform(node, {
|
||||
centerX: item.currentCenterX,
|
||||
centerY: item.currentCenterY,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
rotation: item.displayRotation ?? 0,
|
||||
scale: 1,
|
||||
zIndex: itemEntry?.z,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [
|
||||
if (isGroupDrag) {
|
||||
groupItems.forEach((item) => {
|
||||
if (item.docId === docKey) {
|
||||
return;
|
||||
}
|
||||
const node = itemRefs.current.get(item.docId);
|
||||
if (node) {
|
||||
item.displayRotation = item.initialRotation;
|
||||
const itemEntry = layoutRef.current.get(item.docId) || null;
|
||||
applyDomTransform(node, {
|
||||
centerX: item.currentCenterX,
|
||||
centerY: item.currentCenterY,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
rotation: item.displayRotation ?? 0,
|
||||
scale: 1,
|
||||
zIndex: itemEntry?.z,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [
|
||||
bringToFront,
|
||||
canvasPadding,
|
||||
containerRef,
|
||||
@@ -674,147 +673,147 @@ const useDocumentDrag = (options: UseDocumentDragOptions) => {
|
||||
state.rotation = state.restRotation + state.dynamicRotation;
|
||||
};
|
||||
|
||||
if (state.isGroup) {
|
||||
const containerRect = containerRef.current?.getBoundingClientRect?.();
|
||||
if (containerRect) {
|
||||
state.containerRectLeft = containerRect.left;
|
||||
state.containerRectTop = containerRect.top;
|
||||
}
|
||||
|
||||
const pointerCanvasX = event.clientX - state.containerRectLeft;
|
||||
const pointerCanvasY = event.clientY - state.containerRectTop;
|
||||
const deltaX = event.clientX - state.startX;
|
||||
const deltaY = event.clientY - state.startY;
|
||||
|
||||
if (!state.moved) {
|
||||
const distanceSquared = deltaX * deltaX + deltaY * deltaY;
|
||||
if (distanceSquared < DRAG_HYSTERESIS_SQUARED) {
|
||||
return;
|
||||
if (state.isGroup) {
|
||||
const containerRect = containerRef.current?.getBoundingClientRect?.();
|
||||
if (containerRect) {
|
||||
state.containerRectLeft = containerRect.left;
|
||||
state.containerRectTop = containerRect.top;
|
||||
}
|
||||
state.moved = true;
|
||||
if (
|
||||
!state.stackSelectionApplied
|
||||
&& Array.isArray(state.stackDocIds)
|
||||
&& state.stackDocIds.length > 0
|
||||
) {
|
||||
safeInvoke(onDocumentStackSelect, state.stackDocIds as Identifier[], event, {
|
||||
replace: state.stackReplace,
|
||||
});
|
||||
state.stackSelectionApplied = true;
|
||||
}
|
||||
if (!state.groupElevated) {
|
||||
const layout = layoutRef.current;
|
||||
const sortedGroup = state.activeDocIds
|
||||
.filter((id) => id !== state.docKey)
|
||||
.sort((a, b) => {
|
||||
const aZ = layout.get(a)?.z ?? 0;
|
||||
const bZ = layout.get(b)?.z ?? 0;
|
||||
return aZ - bZ;
|
||||
|
||||
const pointerCanvasX = event.clientX - state.containerRectLeft;
|
||||
const pointerCanvasY = event.clientY - state.containerRectTop;
|
||||
const deltaX = event.clientX - state.startX;
|
||||
const deltaY = event.clientY - state.startY;
|
||||
|
||||
if (!state.moved) {
|
||||
const distanceSquared = deltaX * deltaX + deltaY * deltaY;
|
||||
if (distanceSquared < DRAG_HYSTERESIS_SQUARED) {
|
||||
return;
|
||||
}
|
||||
state.moved = true;
|
||||
if (
|
||||
!state.stackSelectionApplied
|
||||
&& Array.isArray(state.stackDocIds)
|
||||
&& state.stackDocIds.length > 0
|
||||
) {
|
||||
safeInvoke(onDocumentStackSelect, state.stackDocIds as Identifier[], event, {
|
||||
replace: state.stackReplace,
|
||||
});
|
||||
state.stackSelectionApplied = true;
|
||||
}
|
||||
if (!state.groupElevated) {
|
||||
const layout = layoutRef.current;
|
||||
const sortedGroup = state.activeDocIds
|
||||
.filter((id) => id !== state.docKey)
|
||||
.sort((a, b) => {
|
||||
const aZ = layout.get(a)?.z ?? 0;
|
||||
const bZ = layout.get(b)?.z ?? 0;
|
||||
return aZ - bZ;
|
||||
});
|
||||
|
||||
sortedGroup.forEach((id) => bringToFront(id));
|
||||
bringToFront(state.docKey);
|
||||
state.groupElevated = true;
|
||||
sortedGroup.forEach((id) => bringToFront(id));
|
||||
bringToFront(state.docKey);
|
||||
state.groupElevated = true;
|
||||
}
|
||||
}
|
||||
|
||||
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 desiredCenterX = pointerCanvasX - state.localPointerOffsetX;
|
||||
const desiredCenterY = pointerCanvasY - state.localPointerOffsetY;
|
||||
const centerX = clamp(desiredCenterX, minCenterX, maxCenterX);
|
||||
const centerY = clamp(desiredCenterY, minCenterY, maxCenterY);
|
||||
|
||||
state.currentCenterX = centerX;
|
||||
state.currentCenterY = centerY;
|
||||
|
||||
state.groupItems.forEach((item) => {
|
||||
const isPrimary = item.docId === state.docKey;
|
||||
|
||||
if (isPrimary) {
|
||||
item.currentCenterX = centerX;
|
||||
item.currentCenterY = centerY;
|
||||
item.offsetX = item.baseOffsetX ?? 0;
|
||||
item.offsetY = item.baseOffsetY ?? 0;
|
||||
item.displayRotation = state.rotation ?? item.displayRotation ?? 0;
|
||||
} else {
|
||||
const decay = 0.82;
|
||||
const currentOffsetX = (item.offsetX ?? item.baseOffsetX ?? 0) * decay;
|
||||
const currentOffsetY = (item.offsetY ?? item.baseOffsetY ?? 0) * decay;
|
||||
item.offsetX = Math.abs(currentOffsetX) < 0.5 ? 0 : currentOffsetX;
|
||||
item.offsetY = Math.abs(currentOffsetY) < 0.5 ? 0 : currentOffsetY;
|
||||
|
||||
const targetX = centerX + item.offsetX;
|
||||
const targetY = centerY + item.offsetY;
|
||||
const smoothing = 0.18;
|
||||
item.currentCenterX += (targetX - item.currentCenterX) * smoothing;
|
||||
item.currentCenterY += (targetY - item.currentCenterY) * smoothing;
|
||||
|
||||
const halfW = item.width / 2;
|
||||
const halfH = item.height / 2;
|
||||
const minX = canvasPadding + halfW;
|
||||
const maxX = Math.max(minX, canvasWidth - canvasPadding - halfW);
|
||||
const minY = canvasPadding + halfH;
|
||||
const maxY = Math.max(minY, canvasHeight - canvasPadding - halfH);
|
||||
item.currentCenterX = clamp(item.currentCenterX, minX, maxX);
|
||||
item.currentCenterY = clamp(item.currentCenterY, minY, maxY);
|
||||
|
||||
const rotationBlend = 0.16;
|
||||
item.displayRotation += (item.targetRotation - item.displayRotation) * rotationBlend;
|
||||
}
|
||||
|
||||
const entry = layoutRef.current.get(item.docId) || null;
|
||||
const payload = {
|
||||
centerX: item.currentCenterX,
|
||||
centerY: item.currentCenterY,
|
||||
rotation: item.displayRotation ?? 0,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
scale: isPrimary ? state.dragScale || 1 : 1,
|
||||
zIndex: entry?.z,
|
||||
};
|
||||
|
||||
setDragTransform(item.docId, payload);
|
||||
const node = itemRefs.current.get(item.docId);
|
||||
applyDomTransform(node, payload);
|
||||
});
|
||||
|
||||
const currentTimestampGroup =
|
||||
(Number.isFinite(event?.timeStamp))
|
||||
? event.timeStamp
|
||||
: performance?.now
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
const previousTimestampGroup = state.lastTimestamp ?? currentTimestampGroup;
|
||||
let dtGroup = (currentTimestampGroup - previousTimestampGroup) / 1000;
|
||||
if (!Number.isFinite(dtGroup) || dtGroup <= 0) {
|
||||
dtGroup = MIN_TIMESTEP;
|
||||
}
|
||||
dtGroup = clamp(dtGroup, MIN_TIMESTEP, MAX_TIMESTEP);
|
||||
|
||||
state.lastClientX = event.clientX;
|
||||
state.lastClientY = event.clientY;
|
||||
state.lastTimestamp = currentTimestampGroup;
|
||||
|
||||
updatePointerAngularVelocity(pointerCanvasX, pointerCanvasY, centerX, centerY, dtGroup);
|
||||
applyDynamicRotation(dtGroup, 0.96);
|
||||
state.groupItems.forEach((item) => {
|
||||
if (item.docId === state.docKey) {
|
||||
item.displayRotation = state.rotation ?? item.displayRotation ?? 0;
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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 desiredCenterX = pointerCanvasX - state.localPointerOffsetX;
|
||||
const desiredCenterY = pointerCanvasY - state.localPointerOffsetY;
|
||||
const centerX = clamp(desiredCenterX, minCenterX, maxCenterX);
|
||||
const centerY = clamp(desiredCenterY, minCenterY, maxCenterY);
|
||||
|
||||
state.currentCenterX = centerX;
|
||||
state.currentCenterY = centerY;
|
||||
|
||||
state.groupItems.forEach((item) => {
|
||||
const isPrimary = item.docId === state.docKey;
|
||||
|
||||
if (isPrimary) {
|
||||
item.currentCenterX = centerX;
|
||||
item.currentCenterY = centerY;
|
||||
item.offsetX = item.baseOffsetX ?? 0;
|
||||
item.offsetY = item.baseOffsetY ?? 0;
|
||||
item.displayRotation = state.rotation ?? item.displayRotation ?? 0;
|
||||
} else {
|
||||
const decay = 0.82;
|
||||
const currentOffsetX = (item.offsetX ?? item.baseOffsetX ?? 0) * decay;
|
||||
const currentOffsetY = (item.offsetY ?? item.baseOffsetY ?? 0) * decay;
|
||||
item.offsetX = Math.abs(currentOffsetX) < 0.5 ? 0 : currentOffsetX;
|
||||
item.offsetY = Math.abs(currentOffsetY) < 0.5 ? 0 : currentOffsetY;
|
||||
|
||||
const targetX = centerX + item.offsetX;
|
||||
const targetY = centerY + item.offsetY;
|
||||
const smoothing = 0.18;
|
||||
item.currentCenterX += (targetX - item.currentCenterX) * smoothing;
|
||||
item.currentCenterY += (targetY - item.currentCenterY) * smoothing;
|
||||
|
||||
const halfW = item.width / 2;
|
||||
const halfH = item.height / 2;
|
||||
const minX = canvasPadding + halfW;
|
||||
const maxX = Math.max(minX, canvasWidth - canvasPadding - halfW);
|
||||
const minY = canvasPadding + halfH;
|
||||
const maxY = Math.max(minY, canvasHeight - canvasPadding - halfH);
|
||||
item.currentCenterX = clamp(item.currentCenterX, minX, maxX);
|
||||
item.currentCenterY = clamp(item.currentCenterY, minY, maxY);
|
||||
|
||||
const rotationBlend = 0.16;
|
||||
item.displayRotation += (item.targetRotation - item.displayRotation) * rotationBlend;
|
||||
}
|
||||
|
||||
const entry = layoutRef.current.get(item.docId) || null;
|
||||
const payload = {
|
||||
centerX: item.currentCenterX,
|
||||
centerY: item.currentCenterY,
|
||||
rotation: item.displayRotation ?? 0,
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
scale: isPrimary ? state.dragScale || 1 : 1,
|
||||
zIndex: entry?.z,
|
||||
};
|
||||
|
||||
setDragTransform(item.docId, payload);
|
||||
const node = itemRefs.current.get(item.docId);
|
||||
applyDomTransform(node, payload);
|
||||
});
|
||||
|
||||
const currentTimestampGroup =
|
||||
(Number.isFinite(event?.timeStamp))
|
||||
? event.timeStamp
|
||||
: performance?.now
|
||||
? performance.now()
|
||||
: Date.now();
|
||||
const previousTimestampGroup = state.lastTimestamp ?? currentTimestampGroup;
|
||||
let dtGroup = (currentTimestampGroup - previousTimestampGroup) / 1000;
|
||||
if (!Number.isFinite(dtGroup) || dtGroup <= 0) {
|
||||
dtGroup = MIN_TIMESTEP;
|
||||
}
|
||||
dtGroup = clamp(dtGroup, MIN_TIMESTEP, MAX_TIMESTEP);
|
||||
|
||||
state.lastClientX = event.clientX;
|
||||
state.lastClientY = event.clientY;
|
||||
state.lastTimestamp = currentTimestampGroup;
|
||||
|
||||
updatePointerAngularVelocity(pointerCanvasX, pointerCanvasY, centerX, centerY, dtGroup);
|
||||
applyDynamicRotation(dtGroup, 0.96);
|
||||
state.groupItems.forEach((item) => {
|
||||
if (item.docId === state.docKey) {
|
||||
item.displayRotation = state.rotation ?? item.displayRotation ?? 0;
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
if (state.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { clamp, formatTransform } from '../utils/math';
|
||||
import { fetchLayoutRecords, upsertLayoutRecords } from './db';
|
||||
|
||||
type DocumentId = string;
|
||||
import type { DocumentId } from '../types/identifiers';
|
||||
type TenantId = import('../types/identifiers').TenantId;
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
@@ -63,7 +63,7 @@ interface BaseMetrics {
|
||||
}
|
||||
|
||||
interface DragGroupItem {
|
||||
docId?: string | number | null;
|
||||
docId?: string | null;
|
||||
width: number;
|
||||
height: number;
|
||||
currentCenterX?: number;
|
||||
@@ -80,7 +80,7 @@ interface DragState {
|
||||
}
|
||||
|
||||
interface InertiaSimulationState {
|
||||
docId: string;
|
||||
docId: DocumentId;
|
||||
restRotation: number;
|
||||
rotation: number;
|
||||
dynamicRotation: number;
|
||||
@@ -106,7 +106,7 @@ interface WorkspaceSnapshot {
|
||||
|
||||
type WorkspaceSubscriber = () => void;
|
||||
|
||||
type DeskDocument = { id?: string | number | null } & Record<string, unknown>;
|
||||
type DeskDocument = { id?: string | null } & Record<string, unknown>;
|
||||
|
||||
type EnsureDocumentSize = (doc: DeskDocument) => DocumentSize | null;
|
||||
|
||||
@@ -226,7 +226,7 @@ export const clampCardDimensions = (width: number, height: number): CardDimensio
|
||||
};
|
||||
};
|
||||
|
||||
export const computeFallbackCardSize = (docId: string | number): CardDimensions | null => {
|
||||
export const computeFallbackCardSize = (docId: DocumentId): CardDimensions | null => {
|
||||
const baseSeed = seededRandom(`${docId}:fallback-size`);
|
||||
const aspectSeed = seededRandom(`${docId}:fallback-aspect`);
|
||||
|
||||
@@ -259,7 +259,7 @@ function randomRangeFromSeed(seedKey: string, min: number, max: number): number
|
||||
return min + seed * span;
|
||||
}
|
||||
|
||||
function buildKey(docId: string | number, suffix: string): string {
|
||||
function buildKey(docId: DocumentId, suffix: string): string {
|
||||
return `${docId}::${suffix}`;
|
||||
}
|
||||
|
||||
@@ -525,63 +525,34 @@ const generateInitialLayout = (
|
||||
|
||||
export class WorkspaceEngine {
|
||||
allowLayoutPersistence: boolean;
|
||||
|
||||
tenantId: string | null;
|
||||
|
||||
tenantId: TenantId | null;
|
||||
viewId: string | null;
|
||||
|
||||
layout: Map<DocumentId, LayoutEntry>;
|
||||
|
||||
layoutSnapshot: Map<DocumentId, LayoutEntry>;
|
||||
|
||||
persistedLayout: Map<DocumentId, LayoutEntry>;
|
||||
|
||||
layoutDirty: boolean;
|
||||
|
||||
zCounter: number;
|
||||
|
||||
canvasSize: { width: number; height: number };
|
||||
|
||||
visibleDocIds: Set<DocumentId>;
|
||||
|
||||
draggingId: string | null;
|
||||
|
||||
tagDropTargetId: string | null;
|
||||
|
||||
pendingTagDocId: string | null;
|
||||
|
||||
pendingRemovalTag: unknown;
|
||||
|
||||
dragInProgress: boolean;
|
||||
|
||||
activeDragDocIds: Set<DocumentId>;
|
||||
|
||||
pendingSnapshotSync: boolean;
|
||||
|
||||
pendingPersistSync: boolean;
|
||||
|
||||
persistDebounceId: number | null;
|
||||
|
||||
items: DeskDocument[];
|
||||
|
||||
documentLookup: Map<string, DeskDocument>;
|
||||
|
||||
ensureDocumentSize: EnsureDocumentSize;
|
||||
|
||||
resolveBaseMetrics: ResolveBaseMetrics;
|
||||
|
||||
snapshotCache: WorkspaceSnapshot;
|
||||
|
||||
subscribers: Set<WorkspaceSubscriber>;
|
||||
|
||||
loadingPersisted: boolean;
|
||||
|
||||
pendingPersistence: unknown;
|
||||
|
||||
itemRefs: ItemRefs;
|
||||
|
||||
inertiaAnimations: Map<string, InertiaSimulationState>;
|
||||
|
||||
initialLoadDone: boolean;
|
||||
|
||||
constructor({
|
||||
@@ -716,7 +687,7 @@ export class WorkspaceEngine {
|
||||
this.emit();
|
||||
}
|
||||
|
||||
setDraggingId(docId: string | number | null): void {
|
||||
setDraggingId(docId: DocumentId | null): void {
|
||||
const normalized = docId != null ? String(docId) : null;
|
||||
if (this.draggingId === normalized) {
|
||||
return;
|
||||
@@ -725,7 +696,7 @@ export class WorkspaceEngine {
|
||||
this.emit();
|
||||
}
|
||||
|
||||
beginDrag(docIds: Array<string | number | null> = []): void {
|
||||
beginDrag(docIds: Array<string | null> = []): void {
|
||||
this.dragInProgress = true;
|
||||
if (Array.isArray(docIds)) {
|
||||
this.activeDragDocIds = new Set(docIds.map((id) => (id != null ? String(id) : null)).filter(Boolean));
|
||||
@@ -749,7 +720,7 @@ export class WorkspaceEngine {
|
||||
}
|
||||
}
|
||||
|
||||
setTagDropTargetId(docId: string | number | null): void {
|
||||
setTagDropTargetId(docId: DocumentId | null): void {
|
||||
const normalized = docId != null ? String(docId) : null;
|
||||
if (this.tagDropTargetId === normalized) {
|
||||
return;
|
||||
@@ -758,7 +729,7 @@ export class WorkspaceEngine {
|
||||
this.emit();
|
||||
}
|
||||
|
||||
setPendingTagDocId(docId: string | number | null): void {
|
||||
setPendingTagDocId(docId: DocumentId | null): void {
|
||||
const normalized = docId != null ? String(docId) : null;
|
||||
if (this.pendingTagDocId === normalized) {
|
||||
return;
|
||||
@@ -779,7 +750,7 @@ export class WorkspaceEngine {
|
||||
this.layoutDirty = true;
|
||||
}
|
||||
|
||||
getLayout(docId: string | number | null): LayoutEntry | null {
|
||||
getLayout(docId: DocumentId | null): LayoutEntry | null {
|
||||
if (docId == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -788,7 +759,7 @@ export class WorkspaceEngine {
|
||||
}
|
||||
|
||||
updateLayoutEntry(
|
||||
docId: string | number | null,
|
||||
docId: DocumentId | null,
|
||||
updater: (previous: LayoutEntry | null) => LayoutEntry | null,
|
||||
): void {
|
||||
if (docId == null) {
|
||||
@@ -807,7 +778,7 @@ export class WorkspaceEngine {
|
||||
this.persistLayoutSnapshot();
|
||||
}
|
||||
|
||||
bringToFront(docId: string | number | null): void {
|
||||
bringToFront(docId: DocumentId | null): void {
|
||||
if (docId == null) {
|
||||
return;
|
||||
}
|
||||
@@ -825,7 +796,7 @@ export class WorkspaceEngine {
|
||||
}
|
||||
|
||||
applyTransform(
|
||||
docId: string | number | null,
|
||||
docId: DocumentId | null,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
width: number,
|
||||
@@ -896,7 +867,7 @@ export class WorkspaceEngine {
|
||||
this.persistLayoutSnapshot();
|
||||
}
|
||||
|
||||
cancelInertiaAnimation(docId: string | number | null): void {
|
||||
cancelInertiaAnimation(docId: DocumentId | null): void {
|
||||
const key = docId != null ? String(docId) : null;
|
||||
if (!key) {
|
||||
return;
|
||||
@@ -995,7 +966,7 @@ export class WorkspaceEngine {
|
||||
return isSettled;
|
||||
}
|
||||
|
||||
startInertiaAnimation(docId: string | number | null, baseState: InertiaSimulationState): void {
|
||||
startInertiaAnimation(docId: DocumentId | null, baseState: InertiaSimulationState): void {
|
||||
const raf = window.requestAnimationFrame;
|
||||
if (!raf) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user