feat: Introduce useCardPointer for card-specific pointer events, add spatial capabilities to LayoutSystem

This commit is contained in:
2025-11-26 14:02:56 +01:00
parent bbf7d23c96
commit d44c33dd69
3 changed files with 79 additions and 8 deletions
+23 -8
View File
@@ -10,6 +10,7 @@ import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
import DesktopDocumentCard from './DesktopDocumentCard'; import DesktopDocumentCard from './DesktopDocumentCard';
import usePreviewMetadata from './hooks/usePreviewMetadata'; import usePreviewMetadata from './hooks/usePreviewMetadata';
import useDeskTagInteractions from './tags/useDeskTagInteractions'; import useDeskTagInteractions from './tags/useDeskTagInteractions';
import { useCardPointer } from './useCardPointer';
import '../styles/workspace/workspace-layout.css'; import '../styles/workspace/workspace-layout.css';
import '../styles/workspace/workspace-items.css'; import '../styles/workspace/workspace-items.css';
import '../styles/workspace/workspace-cards.css'; import '../styles/workspace/workspace-cards.css';
@@ -54,6 +55,21 @@ export interface DesktopWorkspaceProps {
onDocumentTagDrop?: (docId: Identifier, tag: any) => void; onDocumentTagDrop?: (docId: Identifier, tag: any) => void;
} }
// Wrapper to handle hooks per card
const DesktopDocumentContainer: React.FC<React.ComponentProps<typeof DesktopDocumentCard>> = React.memo((props) => {
const { layoutCard, selected } = props;
// We assume layoutCard is always present in this context
const cardPointerHandlers = useCardPointer(layoutCard!, !!selected);
return (
<DesktopDocumentCard
{...props}
cardPointerHandlers={cardPointerHandlers}
/>
);
});
DesktopDocumentContainer.displayName = 'DesktopDocumentContainer';
const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
entries, entries,
ensureAssetUrl, ensureAssetUrl,
@@ -149,18 +165,18 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
<> <>
<div <div
className="desk-shell" className="desk-shell"
onPointerDown={(e) => {
if (e.target === e.currentTarget) {
onClearSelection();
focusShell();
}
}}
> >
<div <div
className="desk-canvas" className="desk-canvas"
ref={containerRef} ref={containerRef}
tabIndex={0} tabIndex={0}
onKeyDown={handleShellKeyDown} onKeyDown={handleShellKeyDown}
onPointerDown={(e) => {
if (e.target === e.currentTarget) {
onClearSelection();
focusShell();
}
}}
> >
{items.map((doc, index) => { {items.map((doc, index) => {
const docId = doc.id ? String(doc.id) : `temp-${index}`; const docId = doc.id ? String(doc.id) : `temp-${index}`;
@@ -173,7 +189,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
}); });
return ( return (
<DesktopDocumentCard <DesktopDocumentContainer
key={docId} key={docId}
doc={doc} doc={doc}
style={{ style={{
@@ -191,7 +207,6 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
ensureAssetUrl={ensureAssetUrl} ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset} getDocumentAsset={getDocumentAsset}
handleNavigatorSnapshot={() => { }} handleNavigatorSnapshot={() => { }}
cardPointerHandlers={undefined}
onDocumentActivate={onDocumentActivate} onDocumentActivate={onDocumentActivate}
layoutCard={layoutCard} layoutCard={layoutCard}
onTagDragEnter={tagInteractions.handleTagDragEnterDoc} onTagDragEnter={tagInteractions.handleTagDragEnterDoc}
+33
View File
@@ -20,10 +20,14 @@ export class LayoutCard implements LayoutCardState {
height: number = 0; height: number = 0;
ref: HTMLElement | null = null; ref: HTMLElement | null = null;
private _innerRadius: number = 0;
private _outerRadius: number = 0;
constructor(id: string, initialData: Partial<LayoutCardState> = {}, ref: HTMLElement | null = null) { constructor(id: string, initialData: Partial<LayoutCardState> = {}, ref: HTMLElement | null = null) {
this.id = id; this.id = id;
Object.assign(this, initialData); Object.assign(this, initialData);
this.ref = ref; this.ref = ref;
this.recalculateRadii();
} }
setRef(ref: HTMLElement | null) { setRef(ref: HTMLElement | null) {
@@ -33,9 +37,17 @@ export class LayoutCard implements LayoutCardState {
update(changes: Partial<LayoutCardState>) { update(changes: Partial<LayoutCardState>) {
Object.assign(this, changes); Object.assign(this, changes);
if (changes.width !== undefined || changes.height !== undefined) {
this.recalculateRadii();
}
this.applyTransform(); this.applyTransform();
} }
private recalculateRadii() {
this._innerRadius = Math.min(this.width, this.height) / 2;
this._outerRadius = Math.sqrt(this.width * this.width + this.height * this.height) / 2;
}
private applyTransform() { private applyTransform() {
if (this.ref) { if (this.ref) {
this.ref.style.transform = this.ref.style.transform =
@@ -57,6 +69,14 @@ export class LayoutCard implements LayoutCardState {
height: this.height height: this.height
}; };
} }
get innerRadius(): number {
return this._innerRadius;
}
get outerRadius(): number {
return this._outerRadius;
}
} }
export class LayoutStore { export class LayoutStore {
@@ -117,6 +137,19 @@ export class LayoutStore {
this.update(id, { z: ++this.zCounter }); this.update(id, { z: ++this.zCounter });
} }
getCardsInCircle(x: number, y: number, radius: number): LayoutCard[] {
const result: LayoutCard[] = [];
for (const card of this.items.values()) {
const dx = card.x - x;
const dy = card.y - y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < radius + card.outerRadius) {
result.push(card);
}
}
return result;
}
getSnapshot() { getSnapshot() {
return Array.from(this.items.values()).map(card => card.toSnapshot()); return Array.from(this.items.values()).map(card => card.toSnapshot());
} }
+23
View File
@@ -0,0 +1,23 @@
import React, { useCallback } from 'react';
import { LayoutCard } from './LayoutSystem';
export const useCardPointer = (card: LayoutCard, isSelected: boolean) => {
const onPointerDown = useCallback((e: React.PointerEvent) => {
console.log('Pointer down on card', card.id, card.rotation, card.z, isSelected, e);
}, [card, isSelected]);
const onPointerMove = useCallback((e: React.PointerEvent) => {
console.log('Pointer move on card', card.id, card.rotation, e);
}, [card]);
const onPointerUp = useCallback((e: React.PointerEvent) => {
console.log('Pointer up on card', card.id, card.rotation, e);
}, [card]);
return {
onPointerDown,
onPointerMove,
onPointerUp,
};
};