From 827714db9614de015c2821dc9f8d3d86ae7c6f3e Mon Sep 17 00:00:00 2001 From: Grant Forrest Date: Fri, 7 Jun 2024 23:56:59 -0400 Subject: [PATCH 1/5] [sc] use spatial hash for bounds checks --- .../web/src/components/canvas/BoxRegion.tsx | 22 ++-- .../web/src/components/canvas/BoxSelect.tsx | 2 +- .../src/components/canvas/CanvasObject.tsx | 2 + .../web/src/components/canvas/ObjectBounds.ts | 46 +++++++- .../web/src/components/canvas/Selections.ts | 22 ++-- .../web/src/components/canvas/SpatialHash.ts | 100 ++++++++++++++++++ .../web/src/components/project/TaskNode.tsx | 2 +- 7 files changed, 169 insertions(+), 27 deletions(-) create mode 100644 apps/star-chart/web/src/components/canvas/SpatialHash.ts diff --git a/apps/star-chart/web/src/components/canvas/BoxRegion.tsx b/apps/star-chart/web/src/components/canvas/BoxRegion.tsx index 9cfc9d8a..ebd7441c 100644 --- a/apps/star-chart/web/src/components/canvas/BoxRegion.tsx +++ b/apps/star-chart/web/src/components/canvas/BoxRegion.tsx @@ -6,8 +6,8 @@ import { CanvasGestureInfo } from './Canvas.js'; import { useCanvas } from './CanvasProvider.jsx'; export interface BoxRegionProps { - onPending?: (objectIds: string[], info: CanvasGestureInfo) => void; - onEnd?: (objectIds: string[], info: CanvasGestureInfo) => void; + onPending?: (objectIds: Set, info: CanvasGestureInfo) => void; + onEnd?: (objectIds: Set, info: CanvasGestureInfo) => void; tolerance?: number; className?: string; } @@ -26,13 +26,13 @@ export function BoxRegion({ })); const originRef = useRef({ x: 0, y: 0 }); - const previousPending = useRef([]); + const previousPending = useRef>(new Set()); const canvas = useCanvas(); useCanvasGestures({ onDragStart: (info) => { - previousPending.current = []; + previousPending.current = new Set(); originRef.current = info.worldPosition; spring.set({ x: info.worldPosition.x, @@ -52,15 +52,15 @@ export function BoxRegion({ const objectIds = canvas.bounds.getIntersections(rect, tolerance); // this is all just logic to diff as much as possible... - if (objectIds.length !== previousPending.current.length) { + if (objectIds.size !== previousPending.current.size) { onPending?.(objectIds, info); - } else if (objectIds.length === 0) { - if (previousPending.current.length !== 0) { - onPending?.([], info); + } else if (objectIds.size === 0) { + if (previousPending.current.size !== 0) { + onPending?.(objectIds, info); } } else { - for (let i = 0; i < objectIds.length; i++) { - if (objectIds[i] !== previousPending.current[i]) { + for (const entry of objectIds) { + if (!previousPending.current.has(entry)) { onPending?.(objectIds, info); break; } @@ -80,7 +80,7 @@ export function BoxRegion({ tolerance, ); - onPending?.([], info); + onPending?.(new Set(), info); onCommit?.(objectIds, info); spring.set({ x: 0, y: 0, width: 0, height: 0 }); diff --git a/apps/star-chart/web/src/components/canvas/BoxSelect.tsx b/apps/star-chart/web/src/components/canvas/BoxSelect.tsx index 3388a1f0..5c655479 100644 --- a/apps/star-chart/web/src/components/canvas/BoxSelect.tsx +++ b/apps/star-chart/web/src/components/canvas/BoxSelect.tsx @@ -4,7 +4,7 @@ import { Vector2 } from './types.js'; export interface BoxSelectProps { className?: string; - onCommit?: (objectIds: string[], endPosition: Vector2) => void; + onCommit?: (objectIds: Set, endPosition: Vector2) => void; } export function BoxSelect({ className, onCommit }: BoxSelectProps) { diff --git a/apps/star-chart/web/src/components/canvas/CanvasObject.tsx b/apps/star-chart/web/src/components/canvas/CanvasObject.tsx index b707abba..4bdf3d67 100644 --- a/apps/star-chart/web/src/components/canvas/CanvasObject.tsx +++ b/apps/star-chart/web/src/components/canvas/CanvasObject.tsx @@ -206,6 +206,8 @@ export function useCanvasObject({ // block gestures internal to the drag handle for a bit even // after releasing setTimeout(setIsDragging, 100, false); + // update the spatial hash now that the object is settled + canvas.bounds.updateHash(objectId); }, }); diff --git a/apps/star-chart/web/src/components/canvas/ObjectBounds.ts b/apps/star-chart/web/src/components/canvas/ObjectBounds.ts index 9b952082..f1f7ac3e 100644 --- a/apps/star-chart/web/src/components/canvas/ObjectBounds.ts +++ b/apps/star-chart/web/src/components/canvas/ObjectBounds.ts @@ -1,6 +1,7 @@ import { EventSubscriber } from '@a-type/utils'; import { SpringValue } from '@react-spring/web'; import { Box, LiveVector2, Vector2 } from './types.js'; +import { SpatialHash } from './SpatialHash.js'; export interface Bounds { width: SpringValue; @@ -15,12 +16,44 @@ export class ObjectBounds extends EventSubscriber<{ private origins: Map = new Map(); private sizes: Map = new Map(); private sizeObserver; + private spatialHash = new SpatialHash(100); + private spatialHashRecomputeTimers = new Map(); constructor() { super(); this.sizeObserver = new ResizeObserver(this.handleChanges); } + updateHash = (objectId: string) => { + const origin = this.getOrigin(objectId); + const size = this.getSize(objectId); + + if (!origin || !size) { + console.log('no origin or size', objectId, { + origin, + size, + }); + return; + } + + const x = origin.x.get(); + const y = origin.y.get(); + const width = size.width.get(); + const height = size.height.get(); + + this.spatialHash.replace(objectId, { x, y, width, height }); + }; + + private debouncedUpdateHash = (objectId: string) => { + clearTimeout(this.spatialHashRecomputeTimers.get(objectId)); + this.spatialHashRecomputeTimers.set( + objectId, + setTimeout(() => { + this.updateHash(objectId); + }, 500), + ); + }; + private updateSize = ( objectId: string, changes: Partial<{ width: number; height: number }>, @@ -39,6 +72,8 @@ export class ObjectBounds extends EventSubscriber<{ if (changes.height) { bounds.height.set(changes.height); } + + this.debouncedUpdateHash(objectId); }; observe = (objectId: string, element: Element | null) => { @@ -105,9 +140,14 @@ export class ObjectBounds extends EventSubscriber<{ } getIntersections = (box: Box, threshold: number) => { - return this.ids.filter((objectId) => - this.intersects(objectId, box, threshold), - ); + const nearby = this.spatialHash.queryByRect(box); + const intersections = new Set(); + for (const id of nearby) { + if (this.intersects(id, box, threshold)) { + intersections.add(id); + } + } + return intersections; }; hitTest = (point: Vector2) => { diff --git a/apps/star-chart/web/src/components/canvas/Selections.ts b/apps/star-chart/web/src/components/canvas/Selections.ts index ba851b3c..44b02901 100644 --- a/apps/star-chart/web/src/components/canvas/Selections.ts +++ b/apps/star-chart/web/src/components/canvas/Selections.ts @@ -15,7 +15,7 @@ export class Selections extends EventSubscriber<{ this.emit('change', Array.from(this.selectedIds)); }; - addAll = (objectIds: string[]) => { + addAll = (objectIds: Iterable) => { for (const objectId of objectIds) { this.add(objectId); } @@ -66,31 +66,31 @@ export class Selections extends EventSubscriber<{ } }; - set = (objectIds: string[]) => { - const selectedIds = new Set(objectIds); + set = (objectIds: Set | Array) => { + const selectedIds = Array.isArray(objectIds) + ? new Set(objectIds) + : objectIds; for (const objectId of this.selectedIds) { if (!selectedIds.has(objectId)) { this.remove(objectId); } } for (const objectId of objectIds) { - if (!this.selectedIds.has(objectId)) { - this.add(objectId); - } + this.add(objectId); } }; - setPending = (objectIds: string[]) => { - const pendingIds = new Set(objectIds); + setPending = (objectIds: Set | Array) => { + const pendingIds = Array.isArray(objectIds) + ? new Set(objectIds) + : objectIds; for (const objectId of this.pendingIds) { if (!pendingIds.has(objectId)) { this.removePending(objectId); } } for (const objectId of objectIds) { - if (!this.pendingIds.has(objectId)) { - this.addPending(objectId); - } + this.addPending(objectId); } }; } diff --git a/apps/star-chart/web/src/components/canvas/SpatialHash.ts b/apps/star-chart/web/src/components/canvas/SpatialHash.ts new file mode 100644 index 00000000..ac789643 --- /dev/null +++ b/apps/star-chart/web/src/components/canvas/SpatialHash.ts @@ -0,0 +1,100 @@ +export interface SpatialRect { + x: number; + y: number; + width: number; + height: number; +} + +export class SpatialHash { + cellSize: number; + cells: Map>; + rects: Map; + + constructor(cellSize: number) { + this.cellSize = cellSize; + this.cells = new Map>(); + this.rects = new Map(); + } + + private key(x: number, y: number) { + return `${Math.floor(x / this.cellSize)},${Math.floor(y / this.cellSize)}`; + } + + private getCell(x: number, y: number) { + const key = this.key(x, y); + let cell = this.cells.get(key); + if (!cell) { + cell = new Set(); + this.cells.set(key, cell); + } + return cell; + } + + insert(obj: T, rect: SpatialRect) { + const x0 = Math.floor(rect.x / this.cellSize); + const y0 = Math.floor(rect.y / this.cellSize); + const x1 = Math.floor((rect.x + rect.width) / this.cellSize); + const y1 = Math.floor((rect.y + rect.height) / this.cellSize); + + for (let y = y0; y <= y1; y++) { + for (let x = x0; x <= x1; x++) { + this.getCell(x, y).add(obj); + } + } + + this.rects.set(obj, rect); + } + + remove(obj: T) { + const rect = this.rects.get(obj); + if (!rect) { + return; + } + + const x0 = Math.floor(rect.x / this.cellSize); + const y0 = Math.floor(rect.y / this.cellSize); + const x1 = Math.floor((rect.x + rect.width) / this.cellSize); + const y1 = Math.floor((rect.y + rect.height) / this.cellSize); + + for (let y = y0; y <= y1; y++) { + for (let x = x0; x <= x1; x++) { + this.getCell(x, y).delete(obj); + } + } + + this.rects.delete(obj); + } + + replace(obj: T, newRect: SpatialRect) { + this.remove(obj); + this.insert(obj, newRect); + } + + queryByObject(obj: T) { + const rect = this.rects.get(obj); + if (!rect) { + return new Set(); + } + return this.queryByRect(rect); + } + + queryByRect(rect: SpatialRect) { + const result = new Set(); + const x0 = Math.floor(rect.x / this.cellSize); + const y0 = Math.floor(rect.y / this.cellSize); + const x1 = Math.floor((rect.x + rect.width) / this.cellSize); + const y1 = Math.floor((rect.y + rect.height) / this.cellSize); + + for (let y = y0; y <= y1; y++) { + for (let x = x0; x <= x1; x++) { + const cell = this.cells.get(this.key(x, y)); + if (cell) { + for (const obj of cell) { + result.add(obj); + } + } + } + } + return result; + } +} diff --git a/apps/star-chart/web/src/components/project/TaskNode.tsx b/apps/star-chart/web/src/components/project/TaskNode.tsx index b74eec50..0704ef5e 100644 --- a/apps/star-chart/web/src/components/project/TaskNode.tsx +++ b/apps/star-chart/web/src/components/project/TaskNode.tsx @@ -78,7 +78,7 @@ export function TaskNode({ task }: TaskNodeProps) { isPriority(upstreams, downstreams) && 'layer-variants:bg-primary-wash', upstreams > 0 && 'layer-variants:bg-wash', selected && 'layer-variants:border-primary', - !selected && pendingSelect && 'layer-variants:border-primary-wash', + !selected && pendingSelect && 'layer-variants:border-primary-light', !!completedAt && (downstreamUncompleted ? 'opacity-[calc(var(--zoom,1)*var(--zoom,1))]' From 29e697c5671f8df1e35d5b2c70a75be7bd43a8d3 Mon Sep 17 00:00:00 2001 From: Grant Forrest Date: Sat, 8 Jun 2024 12:33:44 -0400 Subject: [PATCH 2/5] [sc] fix drag to add --- apps/star-chart/web/src/components/project/ConnectionSource.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/star-chart/web/src/components/project/ConnectionSource.tsx b/apps/star-chart/web/src/components/project/ConnectionSource.tsx index 8efaa451..c633d4c9 100644 --- a/apps/star-chart/web/src/components/project/ConnectionSource.tsx +++ b/apps/star-chart/web/src/components/project/ConnectionSource.tsx @@ -51,7 +51,7 @@ export function ConnectionSource({ }, 0, ); - const taskId = objectIds.find( + const taskId = [...objectIds].find( (id) => canvas.objectMetadata.get(id)?.type === 'task', ); return taskId ?? null; From 3876444e103ec8781c5cacbb99505a932c2fa58e Mon Sep 17 00:00:00 2001 From: Grant Forrest Date: Sat, 8 Jun 2024 12:47:24 -0400 Subject: [PATCH 3/5] [sc] improve zoom to fit --- .../web/src/components/canvas/ObjectBounds.ts | 61 +++++++++++++++++++ .../web/src/components/canvas/Viewport.ts | 26 +++++++- .../src/components/project/ProjectCanvas.tsx | 38 +++--------- 3 files changed, 93 insertions(+), 32 deletions(-) diff --git a/apps/star-chart/web/src/components/canvas/ObjectBounds.ts b/apps/star-chart/web/src/components/canvas/ObjectBounds.ts index f1f7ac3e..94837a1b 100644 --- a/apps/star-chart/web/src/components/canvas/ObjectBounds.ts +++ b/apps/star-chart/web/src/components/canvas/ObjectBounds.ts @@ -236,4 +236,65 @@ export class ObjectBounds extends EventSubscriber<{ return intersectionArea / testArea > threshold; }; + + /** + * Get the instantaenous bounding box of an object. + */ + getCurrentBounds = (objectId: string): Box | null => { + const origin = this.getOrigin(objectId); + const size = this.getSize(objectId); + + if (!origin && !size) { + return null; + } + + const bounds: Box = { + x: 0, + y: 0, + width: 0, + height: 0, + }; + + if (origin) { + bounds.x = origin.x.get(); + bounds.y = origin.y.get(); + } + if (size) { + bounds.width = size.width.get(); + bounds.height = size.height.get(); + } + + return bounds; + }; + + /** + * Gets the instantaneous rectangle describing the outer + * limits of all tracked objects + */ + getCurrentContainer = () => { + const ids = this.ids; + let container = this.getCurrentBounds(ids[0]); + if (!container) { + return null; + } + + for (let i = 1; i < ids.length; i++) { + const bounds = this.getCurrentBounds(ids[i]); + if (!bounds) { + continue; + } + + container = { + x: Math.min(container.x, bounds.x), + y: Math.min(container.y, bounds.y), + width: Math.max(container.width, bounds.x - container.x + bounds.width), + height: Math.max( + container.height, + bounds.y - container.y + bounds.height, + ), + }; + } + + return container; + }; } diff --git a/apps/star-chart/web/src/components/canvas/Viewport.ts b/apps/star-chart/web/src/components/canvas/Viewport.ts index ae776bd3..e75b74fb 100644 --- a/apps/star-chart/web/src/components/canvas/Viewport.ts +++ b/apps/star-chart/web/src/components/canvas/Viewport.ts @@ -1,5 +1,5 @@ import { EventSubscriber, preventDefault } from '@a-type/utils'; -import { Size, Vector2, RectLimits } from './types.js'; +import { Size, Vector2, RectLimits, Box } from './types.js'; import { addVectors, clamp, @@ -508,4 +508,28 @@ export class Viewport extends EventSubscriber { this.doPan(worldPosition, info); this.doZoom(zoomValue, info); }; + + /** + * Does the best it can to fit the provided area onscreen. + * Area is in world units. + */ + fitOnScreen = ( + bounds: Box, + { + origin = 'control', + margin = 10, + }: { origin?: ViewportEventOrigin; margin?: number }, + ) => { + const width = bounds.width; + const height = bounds.height; + const zoom = Math.min( + this.elementSize.width / (width + margin), + this.elementSize.height / (height + margin), + ); + const center = { + x: bounds.x + width / 2, + y: bounds.y + height / 2, + }; + this.doMove(center, zoom, { origin }); + }; } diff --git a/apps/star-chart/web/src/components/project/ProjectCanvas.tsx b/apps/star-chart/web/src/components/project/ProjectCanvas.tsx index d45d06cd..1979d608 100644 --- a/apps/star-chart/web/src/components/project/ProjectCanvas.tsx +++ b/apps/star-chart/web/src/components/project/ProjectCanvas.tsx @@ -5,6 +5,7 @@ import { BoxSelect } from '../canvas/BoxSelect.jsx'; import { CanvasContext, CanvasGestures, + useCanvas, useCreateCanvas, } from '../canvas/CanvasProvider.jsx'; import { CanvasRenderer } from '../canvas/CanvasRenderer.jsx'; @@ -127,46 +128,21 @@ function useAddTask(projectId: string) { } function useZoomToFit(tasks: Task[]) { - const viewport = useViewport(); + const canvas = useCanvas(); const [hasZoomed, setHasZoomed] = useState(false); useEffect(() => { if (hasZoomed) return; - const bounds = tasks.reduce( - (acc, task) => { - const position = task.get('position').getAll(); - return { - left: Math.min(acc.left, position.x), - top: Math.min(acc.top, position.y), - right: Math.max(acc.right, position.x), - bottom: Math.max(acc.bottom, position.y), - }; - }, - { - left: Infinity, - top: Infinity, - right: -Infinity, - bottom: -Infinity, - }, - ); - const center = { - x: (bounds.left + bounds.right) / 2, - y: (bounds.top + bounds.bottom) / 2, - }; - const width = bounds.right - bounds.left; - const height = bounds.bottom - bounds.top; - const padding = 100; - const zoom = Math.min( - viewport.elementSize.width / (width + padding), - viewport.elementSize.height / (height + padding), - ); // this ensures the move happens after other // effects that ran on the same render. mainly // the canvas viewport setup stuff... requestAnimationFrame(() => { - viewport.doMove(center, zoom); + const bounds = canvas.bounds.getCurrentContainer(); + if (!bounds) return; + + canvas.viewport.fitOnScreen(bounds, { origin: 'control', margin: 10 }); }); setHasZoomed(true); - }, [tasks, viewport, hasZoomed]); + }, [tasks, canvas, hasZoomed]); } function ZoomFitter({ tasks }: { tasks: Task[] }) { From ce9bfb019da61708a1f5224322fad102a2ecabf2 Mon Sep 17 00:00:00 2001 From: Grant Forrest Date: Sat, 8 Jun 2024 12:49:01 -0400 Subject: [PATCH 4/5] [sc] even more ergo --- apps/star-chart/web/src/components/canvas/Canvas.ts | 13 ++++++++++++- .../web/src/components/project/ProjectCanvas.tsx | 5 +---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/star-chart/web/src/components/canvas/Canvas.ts b/apps/star-chart/web/src/components/canvas/Canvas.ts index c79503d1..83ade680 100644 --- a/apps/star-chart/web/src/components/canvas/Canvas.ts +++ b/apps/star-chart/web/src/components/canvas/Canvas.ts @@ -4,7 +4,7 @@ import { clampVector, snap } from './math.js'; import { ObjectBounds } from './ObjectBounds.js'; import { Selections } from './Selections.js'; import { RectLimits, Vector2 } from './types.js'; -import { Viewport, ViewportConfig } from './Viewport.js'; +import { Viewport, ViewportConfig, ViewportEventOrigin } from './Viewport.js'; import { proxy } from 'valtio'; export interface CanvasOptions { @@ -222,5 +222,16 @@ export class Canvas extends EventSubscriber { } }; + zoomToFit = ( + options: { origin?: ViewportEventOrigin; margin?: number } = {}, + ) => { + const bounds = this.bounds.getCurrentContainer(); + if (bounds) { + this.viewport.fitOnScreen(bounds, options); + } else { + this.viewport.doMove(this.center, 1, options); + } + }; + dispose = () => {}; } diff --git a/apps/star-chart/web/src/components/project/ProjectCanvas.tsx b/apps/star-chart/web/src/components/project/ProjectCanvas.tsx index 1979d608..ff6e87ae 100644 --- a/apps/star-chart/web/src/components/project/ProjectCanvas.tsx +++ b/apps/star-chart/web/src/components/project/ProjectCanvas.tsx @@ -136,10 +136,7 @@ function useZoomToFit(tasks: Task[]) { // effects that ran on the same render. mainly // the canvas viewport setup stuff... requestAnimationFrame(() => { - const bounds = canvas.bounds.getCurrentContainer(); - if (!bounds) return; - - canvas.viewport.fitOnScreen(bounds, { origin: 'control', margin: 10 }); + canvas.zoomToFit({ origin: 'control', margin: 10 }); }); setHasZoomed(true); }, [tasks, canvas, hasZoomed]); From a3e5ab7ad902660f9ba39707f680247fd98a0c7d Mon Sep 17 00:00:00 2001 From: Grant Forrest Date: Sun, 9 Jun 2024 14:09:32 -0400 Subject: [PATCH 5/5] [sc] task text entry improvements --- apps/gnocchi/hub/package.json | 2 +- apps/gnocchi/web/package.json | 2 +- apps/marginalia/web/package.json | 2 +- apps/shopping/web/package.json | 2 +- apps/star-chart/web/package.json | 2 +- .../web/src/components/project/TaskNode.tsx | 25 +++- apps/trip-tick/web/package.json | 2 +- blog/package.json | 2 +- packages/client/package.json | 2 +- pnpm-lock.yaml | 122 +++++++++++------- web/package.json | 2 +- 11 files changed, 107 insertions(+), 58 deletions(-) diff --git a/apps/gnocchi/hub/package.json b/apps/gnocchi/hub/package.json index 591c9a72..62e962e6 100644 --- a/apps/gnocchi/hub/package.json +++ b/apps/gnocchi/hub/package.json @@ -20,7 +20,7 @@ "typecheck": "tsc --build tsconfig.json" }, "dependencies": { - "@a-type/ui": "^0.8.18", + "@a-type/ui": "^0.8.19", "@a-type/utils": "^1.0.8", "@tiptap/core": "^2.2.4", "@tiptap/extension-document": "^2.2.4", diff --git a/apps/gnocchi/web/package.json b/apps/gnocchi/web/package.json index 9a97e2fe..37689742 100644 --- a/apps/gnocchi/web/package.json +++ b/apps/gnocchi/web/package.json @@ -14,7 +14,7 @@ "typecheck": "tsc --build tsconfig.json" }, "dependencies": { - "@a-type/ui": "^0.8.18", + "@a-type/ui": "^0.8.19", "@a-type/utils": "^1.0.8", "@biscuits/client": "workspace:*", "@biscuits/error": "workspace:*", diff --git a/apps/marginalia/web/package.json b/apps/marginalia/web/package.json index 095042ab..3417549b 100644 --- a/apps/marginalia/web/package.json +++ b/apps/marginalia/web/package.json @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@a-type/ui": "0.8.18", + "@a-type/ui": "0.8.19", "@a-type/utils": "1.1.0", "@biscuits/client": "workspace:*", "@marginalia.biscuits/verdant": "workspace:*", diff --git a/apps/shopping/web/package.json b/apps/shopping/web/package.json index bca51011..dda10284 100644 --- a/apps/shopping/web/package.json +++ b/apps/shopping/web/package.json @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@a-type/ui": "^0.8.18", + "@a-type/ui": "^0.8.19", "@a-type/utils": "^1.0.8", "@biscuits/client": "workspace:*", "@react-spring/web": "^9.7.3", diff --git a/apps/star-chart/web/package.json b/apps/star-chart/web/package.json index 47903c86..95745eb9 100644 --- a/apps/star-chart/web/package.json +++ b/apps/star-chart/web/package.json @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@a-type/ui": "0.8.18", + "@a-type/ui": "0.8.19", "@a-type/utils": "1.1.2", "@biscuits/client": "workspace:*", "@react-spring/web": "^9.7.3", diff --git a/apps/star-chart/web/src/components/project/TaskNode.tsx b/apps/star-chart/web/src/components/project/TaskNode.tsx index 0704ef5e..4ea9ea9e 100644 --- a/apps/star-chart/web/src/components/project/TaskNode.tsx +++ b/apps/star-chart/web/src/components/project/TaskNode.tsx @@ -81,8 +81,8 @@ export function TaskNode({ task }: TaskNodeProps) { !selected && pendingSelect && 'layer-variants:border-primary-light', !!completedAt && (downstreamUncompleted - ? 'opacity-[calc(var(--zoom,1)*var(--zoom,1))]' - : 'opacity-[calc(var(--zoom,1)*var(--zoom,1)*0.5)]'), + ? 'opacity-[var(--zoom,1)]' + : 'opacity-[calc(var(--zoom,1)*var(--zoom,1))] sm:opacity-[calc(var(--zoom,1)*var(--zoom,1)*0.5)]'), activeConnectionTarget === id && 'bg-accent-light border-accent', )} style={style} @@ -136,6 +136,11 @@ function TaskFullContent({ [client, id, projectId], ); + const canvas = useCanvas(); + const commitAndEndEditing = () => { + canvas.selections.remove(id); + }; + return (
{exclusive ? ( task.set('content', v)} autoSelect autoFocus + textArea + onKeyDown={(ev) => { + if (ev.key === 'Enter') { + ev.preventDefault(); + task.set('content', content); + commitAndEndEditing(); + } + + const isModifiedKeypress = ev.metaKey || ev.ctrlKey; + if (!isModifiedKeypress) { + // capture all keypresses that aren't modified + ev.stopPropagation(); + } + }} /> ) : (
=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001629 + electron-to-chromium: 1.4.796 + node-releases: 2.0.14 + update-browserslist-db: 1.0.16(browserslist@4.23.1) + dev: true + /buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -13827,6 +13838,10 @@ packages: /caniuse-lite@1.0.30001593: resolution: {integrity: sha512-UWM1zlo3cZfkpBysd7AS+z+v007q9G1+fLTUU42rQnY6t2axoogPW/xol6T7juU5EUoOhML4WgBIdG+9yYqAjQ==} + /caniuse-lite@1.0.30001629: + resolution: {integrity: sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==} + dev: true + /capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} dependencies: @@ -14249,7 +14264,7 @@ packages: /core-js-compat@3.37.1: resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} dependencies: - browserslist: 4.23.0 + browserslist: 4.23.1 dev: true /core-util-is@1.0.3: @@ -14771,6 +14786,10 @@ packages: /electron-to-chromium@1.4.690: resolution: {integrity: sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA==} + /electron-to-chromium@1.4.796: + resolution: {integrity: sha512-NglN/xprcM+SHD2XCli4oC6bWe6kHoytcyLKCWXmRL854F0qhPhaYgUswUsglnPxYaNQIg2uMY4BvaomIf3kLA==} + dev: true + /emmet@2.4.7: resolution: {integrity: sha512-O5O5QNqtdlnQM2bmKHtJgyChcrFMgQuulI+WdiOw2NArzprUqqxUW6bgYtKvzKgrsYpuLWalOkdhNP+1jluhCA==} dependencies: @@ -21897,7 +21916,7 @@ packages: - supports-color dev: true - /unocss@0.59.4(postcss@8.4.38)(vite@5.2.12): + /unocss@0.59.4(postcss@8.4.38)(vite@5.2.13): resolution: {integrity: sha512-QmCVjRObvVu/gsGrJGVt0NnrdhFFn314BUZn2WQyXV9rIvHLRmG5bIu0j5vibJkj7ZhFchTrnTM1pTFXP1xt5g==} engines: {node: '>=14'} peerDependencies: @@ -21909,7 +21928,7 @@ packages: vite: optional: true dependencies: - '@unocss/astro': 0.59.4(vite@5.2.12) + '@unocss/astro': 0.59.4(vite@5.2.13) '@unocss/cli': 0.59.4 '@unocss/core': 0.59.4 '@unocss/extractor-arbitrary-variants': 0.59.4 @@ -21928,8 +21947,8 @@ packages: '@unocss/transformer-compile-class': 0.59.4 '@unocss/transformer-directives': 0.59.4 '@unocss/transformer-variant-group': 0.59.4 - '@unocss/vite': 0.59.4(vite@5.2.12) - vite: 5.2.12 + '@unocss/vite': 0.59.4(vite@5.2.13) + vite: 5.2.13 transitivePeerDependencies: - postcss - rollup @@ -21995,6 +22014,17 @@ packages: escalade: 3.1.2 picocolors: 1.0.1 + /update-browserslist-db@1.0.16(browserslist@4.23.1): + resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.23.1 + escalade: 3.1.2 + picocolors: 1.0.1 + dev: true + /upper-case-first@2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} dependencies: @@ -22310,7 +22340,7 @@ packages: - supports-color dev: true - /vite-plugin-pwa@0.19.2(vite@5.2.12)(workbox-build@7.1.1)(workbox-window@7.1.0): + /vite-plugin-pwa@0.19.2(vite@5.2.13)(workbox-build@7.1.1)(workbox-window@7.1.0): resolution: {integrity: sha512-LSQJFPxCAQYbRuSyc9EbRLRqLpaBA9onIZuQFomfUYjWSgHuQLonahetDlPSC9zsxmkSEhQH8dXZN8yL978h3w==} engines: {node: '>=16.0.0'} peerDependencies: @@ -22325,7 +22355,7 @@ packages: debug: 4.3.4 fast-glob: 3.3.2 pretty-bytes: 6.1.1 - vite: 5.2.12 + vite: 5.2.13 workbox-build: 7.1.1 workbox-window: 7.1.0 transitivePeerDependencies: @@ -22573,8 +22603,8 @@ packages: fsevents: 2.3.3 dev: true - /vite@5.2.12: - resolution: {integrity: sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==} + /vite@5.2.13: + resolution: {integrity: sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: diff --git a/web/package.json b/web/package.json index 0de5c2b4..9b8bc904 100644 --- a/web/package.json +++ b/web/package.json @@ -9,7 +9,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@a-type/ui": "^0.8.18", + "@a-type/ui": "^0.8.19", "@biscuits/apps": "workspace:*", "@biscuits/client": "workspace:*", "@biscuits/error": "workspace:*",