diff --git a/kb-viz/frontend/src/frames/SummaryFrame.tsx b/kb-viz/frontend/src/frames/SummaryFrame.tsx index 78c0a95..2b30339 100644 --- a/kb-viz/frontend/src/frames/SummaryFrame.tsx +++ b/kb-viz/frontend/src/frames/SummaryFrame.tsx @@ -91,24 +91,45 @@ export function SummaryFrame(_props: FrameProps) { {/* ── Current selection ── */}
-

selection · {selected.size}

+
+

+ selection · {selected.size} + {selected.size === 0 && shift+click to add} +

+ {selected.size > 0 && ( + + )} +
{selected.size === 0 ? Nothing selected : selected.size > 8 - ? + ? selectionStore.getState().clear()} /> : selectedNodes.map((n) => ( - +
+ + +
))}
@@ -127,16 +148,24 @@ export function SummaryFrame(_props: FrameProps) { ); } -function SelectionSummary({ nodes }: { nodes: Node[] }) { +function SelectionSummary({ nodes, onClearAll }: { nodes: Node[]; onClearAll: () => void }) { const byType = nodes.reduce>((acc, n) => { acc[n.type] = (acc[n.type] ?? 0) + 1; return acc; }, {}); return ( -
- {Object.entries(byType).map(([type, count]) => ( - {count} {type}{count !== 1 ? 's' : ''} - ))} +
+
+ {Object.entries(byType).map(([type, count]) => ( + {count} {type}{count !== 1 ? 's' : ''} + ))} +
+
); } diff --git a/kb-viz/frontend/src/frames/TimelineFrame.tsx b/kb-viz/frontend/src/frames/TimelineFrame.tsx index 94ca6f7..9485983 100644 --- a/kb-viz/frontend/src/frames/TimelineFrame.tsx +++ b/kb-viz/frontend/src/frames/TimelineFrame.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState, useCallback } from 'react'; +import { useMemo, useState, useCallback, useRef } from 'react'; import DeckGL from '@deck.gl/react'; import { ScatterplotLayer, LineLayer, TextLayer } from '@deck.gl/layers'; import { OrthographicView } from '@deck.gl/core'; @@ -42,8 +42,9 @@ export function TimelineFrame({ width: _w, height: _h }: FrameProps) { const scopedIds = useScopedIds(level); const dateRange = useStore(filterStore, (s) => s.dateRange); - // Brush state: [startX, endX] in data space (ms) + // Brush state: [startX, endX] in data space (ms) + whether shift was held at drag start const [brush, setBrush] = useState<[number, number] | null>(null); + const brushShiftRef = useRef(false); const encode = useMemo( () => makeColorEncoder(nodesById, nodeTypes, colorBy), @@ -166,9 +167,10 @@ export function TimelineFrame({ width: _w, height: _h }: FrameProps) { transitions: { getFillColor: 120 }, }), ]} - onDragStart={(info) => { + onDragStart={(info, event) => { if (!info.coordinate) return; const x = info.coordinate[0]; + brushShiftRef.current = !!(event?.srcEvent as MouseEvent | undefined)?.shiftKey; setBrush([x, x]); }} onDrag={(info) => { @@ -179,9 +181,9 @@ export function TimelineFrame({ width: _w, height: _h }: FrameProps) { if (!brush) return; const [a, b] = [Math.min(brush[0], info.coordinate?.[0] ?? brush[1]), Math.max(brush[0], info.coordinate?.[0] ?? brush[1])]; if (b - a > 1000 * 60 * 60 * 24 * 30) { - // Meaningful range (> 30 days) → apply filter const ids = points.filter((p) => p.x >= a && p.x <= b).map((p) => p.id); - selectionStore.getState().boxSelect(ids); + if (brushShiftRef.current) selectionStore.getState().addToSelection(ids); + else selectionStore.getState().boxSelect(ids); filterStore.getState().setDateRange({ startMs: a, endMs: b }); } setBrush(null);