refactor: introduce dedicated identifier types for improved clarity and type safety

This commit is contained in:
2025-11-24 23:47:36 +01:00
parent 22f0c040a2
commit 70e7bda87e
79 changed files with 1567 additions and 1572 deletions
+9 -8
View File
@@ -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;
+2 -3
View File
@@ -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;
+7 -8
View File
@@ -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));
+30 -6
View File
@@ -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,
+4 -3
View File
@@ -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;
+178 -179
View File
@@ -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;
}
+18 -47
View File
@@ -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;