@fjader/svelte 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { getContext, onMount } from 'svelte';
2
+ import { getContext, onMount, untrack } from 'svelte';
3
3
  import { isBetween } from '@uwdata/mosaic-sql';
4
4
  import { BrushState1D } from './brush-state.svelte';
5
5
  import BrushOverlay from './BrushOverlay.svelte';
@@ -9,6 +9,7 @@
9
9
  isDragging,
10
10
  scaleBrushExtent1D,
11
11
  createPanModeHandlers,
12
+ brushHandleAt,
12
13
  WHEEL_SENSITIVITY
13
14
  } from './brush-utils';
14
15
  import type { ChartContext, FieldRef, Point, D3Scale } from '../types';
@@ -103,7 +104,7 @@
103
104
  let current = 0;
104
105
  let isCapturing = false;
105
106
  let hasDragged = false;
106
- let isPanModeActive = $state(false);
107
+ let isPanModeActive = false;
107
108
 
108
109
  // Reusable point object to reduce GC pressure
109
110
  const point = { x: 0, y: 0 };
@@ -113,9 +114,12 @@
113
114
  let lastEmittedMax: number | null = null;
114
115
 
115
116
  // Keyboard handlers for Space key pan mode
116
- const { handleKeyDown, handleKeyUp } = createPanModeHandlers((active) => {
117
- isPanModeActive = active;
118
- });
117
+ const { handleKeyDown, handleKeyUp, handleBlur } = createPanModeHandlers(
118
+ (active) => {
119
+ isPanModeActive = active;
120
+ },
121
+ () => chart.reroutePress()
122
+ );
119
123
 
120
124
  // Scale validation
121
125
  // Always validate as 'linear': Fjader time scales use float offsets, not Dates,
@@ -128,19 +132,26 @@
128
132
  });
129
133
 
130
134
  // Watch for external selection changes (from detail chart pan/zoom)
131
- // to keep brush visual in sync and update Mosaic filter
135
+ // to keep brush visual in sync and update Mosaic filter.
136
+ // Tracks only the selection extent and its source. The rest runs untracked:
137
+ // emitting the clause reads state it then writes (the filter group's
138
+ // selection, the scales' active values), which made this effect re-run
139
+ // itself without end while a detail chart was panned.
132
140
  $effect(() => {
133
141
  if (!(scaleState instanceof LinearScaleState)) return;
134
142
  const extent = scaleState.selectionExtent;
135
- if (extent && scaleState.selectionSource === 'external') {
136
- brush.setExtent(extent[0], extent[1]);
137
- lastEmittedMin = extent[0];
138
- lastEmittedMax = extent[1];
139
- emitFilterClause(extent[0], extent[1]);
140
- } else if (!extent && brush.active) {
141
- // Selection was cleared externally (e.g. detail chart reset) — clear brush
142
- clearBrush();
143
- }
143
+ const source = scaleState.selectionSource;
144
+ untrack(() => {
145
+ if (extent && source === 'external') {
146
+ brush.setExtent(extent[0], extent[1]);
147
+ lastEmittedMin = extent[0];
148
+ lastEmittedMax = extent[1];
149
+ emitFilterClause(extent[0], extent[1]);
150
+ } else if (!extent && brush.active) {
151
+ // Selection was cleared externally (e.g. detail chart reset) — clear brush
152
+ clearBrush();
153
+ }
154
+ });
144
155
  });
145
156
 
146
157
  // Derived screen coordinates — read chart.xScale/yScale directly so the
@@ -223,16 +234,25 @@
223
234
 
224
235
  // --- Pointer Event Handlers (registered as chart context callbacks) ---
225
236
 
226
- function handlePointerDown(event: PointerEvent, innerPoint: Point) {
237
+ /** A press: move or resize on the brush's handles, else draw; decline in pan mode. */
238
+ function handlePointerDown(target: Element, pointerId: number, innerPoint: Point) {
227
239
  if (brush.phase !== 'idle') return false;
228
240
  if (isPanModeActive) return false;
229
241
 
230
- (event.target as Element).setPointerCapture(event.pointerId);
242
+ target.setPointerCapture(pointerId);
231
243
  isCapturing = true;
232
244
  hasDragged = false;
233
245
 
234
246
  anchor = innerCoord(innerPoint);
235
247
  current = anchor;
248
+
249
+ const hit = brushHandleAt(target);
250
+ if (hit?.handle === 'move') {
251
+ brush.phase = 'moving';
252
+ } else if (hit) {
253
+ brush.phase = 'resizing';
254
+ brush.resizeEdge = hit.handle === edgeStart ? 'start' : 'end';
255
+ }
236
256
  return true;
237
257
  }
238
258
 
@@ -280,44 +300,30 @@
280
300
  (event.target as Element).releasePointerCapture(event.pointerId);
281
301
  isCapturing = false;
282
302
 
303
+ // A press that never moved is a click, which clears the brush.
283
304
  if (!hasDragged && brush.active && (brush.phase === 'idle' || brush.phase === 'moving')) {
284
305
  clearBrush();
285
306
  return true;
286
307
  }
287
308
 
309
+ endDrag();
310
+ return true;
311
+ }
312
+
313
+ /** Keep what the drag did and go idle. */
314
+ function endDrag() {
288
315
  if (brush.phase !== 'idle') {
289
316
  emitUpdate();
290
317
  brush.phase = 'idle';
291
318
  }
292
-
293
319
  hasDragged = false;
294
- return true;
295
320
  }
296
321
 
297
- function handleMoveStart(event: PointerEvent) {
298
- event.stopPropagation();
299
- (event.target as Element).setPointerCapture(event.pointerId);
300
- isCapturing = true;
301
- hasDragged = false;
302
-
303
- point.x = event.offsetX;
304
- point.y = event.offsetY;
305
- const innerCoords = chart.toInnerCoordinates(point);
306
- anchor = innerCoord(innerCoords);
307
- brush.phase = 'moving';
308
- }
309
-
310
- function handleResizeStart(edge: string, event: PointerEvent) {
311
- event.stopPropagation();
312
- (event.target as Element).setPointerCapture(event.pointerId);
313
- isCapturing = true;
314
-
315
- point.x = event.offsetX;
316
- point.y = event.offsetY;
317
- const innerCoords = chart.toInnerCoordinates(point);
318
- anchor = innerCoord(innerCoords);
319
- brush.phase = 'resizing';
320
- brush.resizeEdge = edge === edgeStart ? 'start' : 'end';
322
+ /** The chart re-routes the press: end the drag as it stands, never as a click. */
323
+ function handleDragEnd() {
324
+ if (!isCapturing) return;
325
+ isCapturing = false;
326
+ endDrag();
321
327
  }
322
328
 
323
329
  // Register pointer callbacks; the keys are on <svelte:window> below.
@@ -325,19 +331,21 @@
325
331
  chart.onInteractorPointerDown = handlePointerDown;
326
332
  chart.onInteractorPointerMove = handlePointerMove;
327
333
  chart.onInteractorPointerUp = handlePointerUp;
334
+ chart.onInteractorDragEnd = handleDragEnd;
328
335
  chart.activators.add(brushHandle.activate);
329
336
 
330
337
  return () => {
331
338
  chart.onInteractorPointerDown = null;
332
339
  chart.onInteractorPointerMove = null;
333
340
  chart.onInteractorPointerUp = null;
341
+ chart.onInteractorDragEnd = null;
334
342
  chart.activators.delete(brushHandle.activate);
335
343
  brushHandle.destroy();
336
344
  };
337
345
  });
338
346
  </script>
339
347
 
340
- <svelte:window onkeydown={handleKeyDown} onkeyup={handleKeyUp} />
348
+ <svelte:window onkeydown={handleKeyDown} onkeyup={handleKeyUp} onblur={handleBlur} />
341
349
 
342
350
  {#if brush.active && brushScreen}
343
351
  <BrushOverlay
@@ -346,8 +354,6 @@
346
354
  width={isX ? brushScreen.size : chart.innerWidth}
347
355
  height={isX ? chart.innerHeight : brushScreen.size}
348
356
  orientation={axis}
349
- onMoveStart={handleMoveStart}
350
- onResizeStart={handleResizeStart}
351
357
  onWheel={handleWheel}
352
358
  />
353
359
  {/if}
@@ -8,6 +8,8 @@
8
8
  closeTo,
9
9
  isDragging,
10
10
  scaleBrushExtent2D,
11
+ createPanModeHandlers,
12
+ brushHandleAt,
11
13
  WHEEL_SENSITIVITY
12
14
  } from './brush-utils';
13
15
  import type { ChartContext, FieldRef, Point } from '../types';
@@ -104,6 +106,15 @@
104
106
  let currentY = 0;
105
107
  let isCapturing = false;
106
108
  let hasDragged = false;
109
+ let isPanModeActive = false;
110
+
111
+ // Space held: presses pan the chart instead of brushing
112
+ const { handleKeyDown, handleKeyUp, handleBlur } = createPanModeHandlers(
113
+ (active) => {
114
+ isPanModeActive = active;
115
+ },
116
+ () => chart.reroutePress()
117
+ );
107
118
 
108
119
  // Reusable point object to reduce GC pressure
109
120
  const point = { x: 0, y: 0 };
@@ -218,10 +229,12 @@
218
229
 
219
230
  // --- Pointer Event Handlers (registered as chart context callbacks) ---
220
231
 
221
- function handlePointerDown(event: PointerEvent, innerPoint: Point) {
232
+ /** A press: move or resize on the brush's handles, else draw; decline in pan mode. */
233
+ function handlePointerDown(target: Element, pointerId: number, innerPoint: Point) {
222
234
  if (brush.phase !== 'idle') return false;
235
+ if (isPanModeActive) return false;
223
236
 
224
- (event.target as Element).setPointerCapture(event.pointerId);
237
+ target.setPointerCapture(pointerId);
225
238
  isCapturing = true;
226
239
  hasDragged = false;
227
240
 
@@ -229,6 +242,23 @@
229
242
  anchorY = innerPoint.y;
230
243
  currentX = innerPoint.x;
231
244
  currentY = innerPoint.y;
245
+
246
+ const hit = brushHandleAt(target);
247
+ if (hit?.handle === 'move') {
248
+ brush.phase = 'moving';
249
+ } else if (hit) {
250
+ brush.phase = 'resizing';
251
+ const edge = hit.handle;
252
+ if (edge === 'nw' || edge === 'ne' || edge === 'sw' || edge === 'se') {
253
+ brush.resizeCorner = edge;
254
+ brush.x.resizeEdge = null;
255
+ brush.y.resizeEdge = null;
256
+ } else {
257
+ brush.resizeCorner = null;
258
+ brush.x.resizeEdge = edge === 'left' || edge === 'right' ? edge : null;
259
+ brush.y.resizeEdge = edge === 'top' || edge === 'bottom' ? edge : null;
260
+ }
261
+ }
232
262
  return true;
233
263
  }
234
264
 
@@ -302,11 +332,18 @@
302
332
  (event.target as Element).releasePointerCapture(event.pointerId);
303
333
  isCapturing = false;
304
334
 
335
+ // A press that never moved is a click, which clears the brush.
305
336
  if (!hasDragged && brush.active && (brush.phase === 'idle' || brush.phase === 'moving')) {
306
337
  clearBrush();
307
338
  return true;
308
339
  }
309
340
 
341
+ endDrag();
342
+ return true;
343
+ }
344
+
345
+ /** Keep what the drag did and go idle. */
346
+ function endDrag() {
310
347
  if (brush.phase !== 'idle') {
311
348
  emitClause();
312
349
  brush.phase = 'idle';
@@ -314,58 +351,14 @@
314
351
  brush.x.resizeEdge = null;
315
352
  brush.y.resizeEdge = null;
316
353
  }
317
-
318
354
  hasDragged = false;
319
- return true;
320
- }
321
-
322
- function handleMoveStart(event: PointerEvent) {
323
- event.stopPropagation();
324
- (event.target as Element).setPointerCapture(event.pointerId);
325
- isCapturing = true;
326
- hasDragged = false;
327
-
328
- point.x = event.offsetX;
329
- point.y = event.offsetY;
330
- const innerCoords = chart.toInnerCoordinates(point);
331
- anchorX = innerCoords.x;
332
- anchorY = innerCoords.y;
333
- currentX = innerCoords.x;
334
- currentY = innerCoords.y;
335
- brush.phase = 'moving';
336
355
  }
337
356
 
338
- function handleResizeStart(
339
- edge: 'left' | 'right' | 'top' | 'bottom' | 'nw' | 'ne' | 'sw' | 'se',
340
- event: PointerEvent
341
- ) {
342
- event.stopPropagation();
343
- (event.target as HTMLElement).setPointerCapture(event.pointerId);
344
- isCapturing = true;
345
-
346
- point.x = event.offsetX;
347
- point.y = event.offsetY;
348
- const innerCoords = chart.toInnerCoordinates(point);
349
- anchorX = innerCoords.x;
350
- anchorY = innerCoords.y;
351
- currentX = innerCoords.x;
352
- currentY = innerCoords.y;
353
- brush.phase = 'resizing';
354
-
355
- if (edge === 'nw' || edge === 'ne' || edge === 'sw' || edge === 'se') {
356
- brush.resizeCorner = edge;
357
- brush.x.resizeEdge = null;
358
- brush.y.resizeEdge = null;
359
- } else {
360
- brush.resizeCorner = null;
361
- if (edge === 'left' || edge === 'right') {
362
- brush.x.resizeEdge = edge;
363
- brush.y.resizeEdge = null;
364
- } else {
365
- brush.x.resizeEdge = null;
366
- brush.y.resizeEdge = edge;
367
- }
368
- }
357
+ /** The chart re-routes the press: end the drag as it stands, never as a click. */
358
+ function handleDragEnd() {
359
+ if (!isCapturing) return;
360
+ isCapturing = false;
361
+ endDrag();
369
362
  }
370
363
 
371
364
  // Register callbacks
@@ -373,18 +366,22 @@
373
366
  chart.onInteractorPointerDown = handlePointerDown;
374
367
  chart.onInteractorPointerMove = handlePointerMove;
375
368
  chart.onInteractorPointerUp = handlePointerUp;
369
+ chart.onInteractorDragEnd = handleDragEnd;
376
370
  chart.activators.add(brushHandle.activate);
377
371
 
378
372
  return () => {
379
373
  chart.onInteractorPointerDown = null;
380
374
  chart.onInteractorPointerMove = null;
381
375
  chart.onInteractorPointerUp = null;
376
+ chart.onInteractorDragEnd = null;
382
377
  chart.activators.delete(brushHandle.activate);
383
378
  brushHandle.destroy();
384
379
  };
385
380
  });
386
381
  </script>
387
382
 
383
+ <svelte:window onkeydown={handleKeyDown} onkeyup={handleKeyUp} onblur={handleBlur} />
384
+
388
385
  {#if brush.active && brushScreen}
389
386
  <BrushOverlay
390
387
  x={brushScreen.x}
@@ -392,8 +389,6 @@
392
389
  width={brushScreen.width}
393
390
  height={brushScreen.height}
394
391
  orientation="2d"
395
- onMoveStart={handleMoveStart}
396
- onResizeStart={handleResizeStart}
397
392
  onWheel={handleWheel}
398
393
  />
399
394
  {/if}
@@ -4,7 +4,7 @@
4
4
  import { SvelteMap } from 'svelte/reactivity';
5
5
  import BrushOverlay from './BrushOverlay.svelte';
6
6
  import ParallelAxisSource, { type ParallelAxis } from './ParallelAxisSource.svelte';
7
- import { closeTo, isDragging, createPanModeHandlers } from './brush-utils';
7
+ import { closeTo, isDragging, createPanModeHandlers, brushHandleAt } from './brush-utils';
8
8
  import { LinearScaleState } from '../scales/LinearScaleState.svelte';
9
9
  import type { ChartContext, Point } from '../types';
10
10
  import type { FilterGroupRegistry } from '../query-engine/FilterGroupRegistry.svelte';
@@ -62,10 +62,13 @@
62
62
  }
63
63
 
64
64
  // Pan mode (Space key) — when active, pointer events fall through to ChartCell for pan
65
- let isPanModeActive = $state(false);
66
- const { handleKeyDown, handleKeyUp } = createPanModeHandlers((active) => {
67
- isPanModeActive = active;
68
- });
65
+ let isPanModeActive = false;
66
+ const { handleKeyDown, handleKeyUp, handleBlur } = createPanModeHandlers(
67
+ (active) => {
68
+ isPanModeActive = active;
69
+ },
70
+ () => chart.reroutePress()
71
+ );
69
72
 
70
73
  // The axis the user is dragging on, by column
71
74
  let activeAxis: string | null = null;
@@ -166,19 +169,35 @@
166
169
 
167
170
  // --- Pointer handlers ---
168
171
 
169
- function handlePointerDown(event: PointerEvent, innerPoint: Point) {
172
+ /**
173
+ * A press: move or resize on an axis's brush handles (that axis, from the
174
+ * overlay's owner), else draw on the nearest axis; decline in pan mode.
175
+ */
176
+ function handlePointerDown(target: Element, pointerId: number, innerPoint: Point) {
170
177
  if (!ps) return false;
171
178
  if (isPanModeActive) return false; // Fall through to ChartCell for parallel axis pan
172
179
 
173
- const col = nearestAxis(innerPoint.x);
180
+ const hit = brushHandleAt(target);
181
+ const col = hit?.owner ?? nearestAxis(innerPoint.x);
174
182
  if (col == null || !axes.has(col)) return false;
183
+ // A drag that starts on a brush overlay hovers its axis, even without a
184
+ // pointer move over the axis first.
185
+ if (hit) hoverAxis(col);
175
186
 
176
- (event.target as Element).setPointerCapture(event.pointerId);
187
+ target.setPointerCapture(pointerId);
177
188
  activeAxis = col;
178
189
  isCapturing = true;
179
190
  hasDragged = false;
180
191
  anchor = innerPoint.y;
181
192
  current = anchor;
193
+
194
+ const brush = axes.get(col)!.brush;
195
+ if (hit?.handle === 'move') {
196
+ brush.phase = 'moving';
197
+ } else if (hit) {
198
+ brush.phase = 'resizing';
199
+ brush.resizeEdge = hit.handle === 'top' ? 'start' : 'end';
200
+ }
182
201
  return true;
183
202
  }
184
203
 
@@ -240,59 +259,39 @@
240
259
  const col = activeAxis;
241
260
  activeAxis = null;
242
261
  const brush = axes.get(col)?.brush;
243
- if (!brush) {
244
- hasDragged = false;
245
- return true;
246
- }
247
262
 
248
- if (!hasDragged && brush.active && (brush.phase === 'idle' || brush.phase === 'moving')) {
263
+ // A press that never moved is a click, which clears the brush.
264
+ if (
265
+ brush &&
266
+ !hasDragged &&
267
+ brush.active &&
268
+ (brush.phase === 'idle' || brush.phase === 'moving')
269
+ ) {
249
270
  clearBrush(col);
250
271
  return true;
251
272
  }
252
273
 
253
- if (brush.phase !== 'idle') {
254
- emitUpdate(col);
255
- brush.phase = 'idle';
256
- }
257
-
258
- hasDragged = false;
274
+ endDrag(col);
259
275
  return true;
260
276
  }
261
277
 
262
- function handleMoveStart(col: string, event: PointerEvent) {
278
+ /** Keep what the drag on an axis did and go idle. */
279
+ function endDrag(col: string) {
263
280
  const brush = axes.get(col)?.brush;
264
- if (!brush) return;
265
- event.stopPropagation();
266
- // A drag that starts on a brush overlay hovers its axis, even without a
267
- // pointer move over the axis first.
268
- hoverAxis(col);
269
- (event.target as Element).setPointerCapture(event.pointerId);
270
- activeAxis = col;
271
- isCapturing = true;
281
+ if (brush && brush.phase !== 'idle') {
282
+ emitUpdate(col);
283
+ brush.phase = 'idle';
284
+ }
272
285
  hasDragged = false;
273
-
274
- point.x = event.offsetX;
275
- point.y = event.offsetY;
276
- const innerCoords = chart.toInnerCoordinates(point);
277
- anchor = innerCoords.y;
278
- brush.phase = 'moving';
279
286
  }
280
287
 
281
- function handleResizeStart(col: string, edge: string, event: PointerEvent) {
282
- const brush = axes.get(col)?.brush;
283
- if (!brush) return;
284
- event.stopPropagation();
285
- hoverAxis(col);
286
- (event.target as Element).setPointerCapture(event.pointerId);
287
- activeAxis = col;
288
- isCapturing = true;
289
-
290
- point.x = event.offsetX;
291
- point.y = event.offsetY;
292
- const innerCoords = chart.toInnerCoordinates(point);
293
- anchor = innerCoords.y;
294
- brush.phase = 'resizing';
295
- brush.resizeEdge = edge === 'top' ? 'start' : 'end';
288
+ /** The chart re-routes the press: end the drag as it stands, never as a click. */
289
+ function handleDragEnd() {
290
+ if (!isCapturing || activeAxis == null) return;
291
+ isCapturing = false;
292
+ const col = activeAxis;
293
+ activeAxis = null;
294
+ endDrag(col);
296
295
  }
297
296
 
298
297
  // Brush screen extents, one per active brush, at its axis's x-position —
@@ -324,18 +323,20 @@
324
323
  chart.onInteractorPointerDown = handlePointerDown;
325
324
  chart.onInteractorPointerMove = handlePointerMove;
326
325
  chart.onInteractorPointerUp = handlePointerUp;
326
+ chart.onInteractorDragEnd = handleDragEnd;
327
327
  chart.activators.add(activateHoveredAxis);
328
328
 
329
329
  return () => {
330
330
  chart.onInteractorPointerDown = null;
331
331
  chart.onInteractorPointerMove = null;
332
332
  chart.onInteractorPointerUp = null;
333
+ chart.onInteractorDragEnd = null;
333
334
  chart.activators.delete(activateHoveredAxis);
334
335
  };
335
336
  });
336
337
  </script>
337
338
 
338
- <svelte:window onkeydown={handleKeyDown} onkeyup={handleKeyUp} />
339
+ <svelte:window onkeydown={handleKeyDown} onkeyup={handleKeyUp} onblur={handleBlur} />
339
340
 
340
341
  {#if ps}
341
342
  {#each ps.columns as col (col)}
@@ -356,8 +357,7 @@
356
357
  width={brushHalfWidth * 2}
357
358
  height={screen.size}
358
359
  orientation="y"
359
- onMoveStart={(e) => handleMoveStart(screen.col, e)}
360
- onResizeStart={(edge, e) => handleResizeStart(screen.col, edge, e)}
360
+ owner={screen.col}
361
361
  onWheel={() => {}}
362
362
  />
363
363
  {/each}
@@ -4,10 +4,30 @@ export declare const EPS_ABSOLUTE = 1e-12;
4
4
  export declare const DRAG_THRESHOLD = 3;
5
5
  export declare const WHEEL_SENSITIVITY = 0.001;
6
6
  export declare function closeTo(a: number | null | undefined, b: number | null | undefined): boolean;
7
- export declare function createPanModeHandlers(setActive: (active: boolean) => void): {
7
+ /**
8
+ * Space-held pan mode for brushes, wired to <svelte:window> keydown, keyup and
9
+ * blur. Each change calls `reroutePress` (the chart's), so a press already
10
+ * under way switches between panning and brushing where the pointer is now:
11
+ * with three-finger drag on a trackpad, one press can last seconds after the
12
+ * fingers lift. Key repeats while Space is held change nothing. Blur ends pan
13
+ * mode: a Space released while the window is unfocused sends no keyup.
14
+ */
15
+ export declare function createPanModeHandlers(setActive: (active: boolean) => void, reroutePress: () => void): {
8
16
  handleKeyDown: (event: KeyboardEvent) => void;
9
17
  handleKeyUp: (event: KeyboardEvent) => void;
18
+ handleBlur: () => void;
10
19
  };
20
+ /** The parts of a brush overlay a press can land on, from its `data-brush-handle`. */
21
+ export type BrushHandle = 'move' | 'left' | 'right' | 'top' | 'bottom' | 'nw' | 'ne' | 'sw' | 'se';
22
+ /**
23
+ * The brush handle under a press, read from the overlay's DOM: its element's
24
+ * `data-brush-handle`, and the `data-brush-owner` of its overlay (a parallel
25
+ * axis's column), or null when the press is not on a brush.
26
+ */
27
+ export declare function brushHandleAt(target: Element | null): {
28
+ handle: BrushHandle;
29
+ owner: string | null;
30
+ } | null;
11
31
  export declare function scaleBrushExtent1D(min: number, max: number, scaleFactor: number): [number, number];
12
32
  export declare function scaleBrushExtent2D(xMin: number, xMax: number, yMin: number, yMax: number, scaleFactor: number): {
13
33
  x: [number, number];
@@ -15,18 +15,46 @@ export function closeTo(a, b) {
15
15
  }
16
16
  return diff < EPS_ABSOLUTE;
17
17
  }
18
- export function createPanModeHandlers(setActive) {
18
+ /**
19
+ * Space-held pan mode for brushes, wired to <svelte:window> keydown, keyup and
20
+ * blur. Each change calls `reroutePress` (the chart's), so a press already
21
+ * under way switches between panning and brushing where the pointer is now:
22
+ * with three-finger drag on a trackpad, one press can last seconds after the
23
+ * fingers lift. Key repeats while Space is held change nothing. Blur ends pan
24
+ * mode: a Space released while the window is unfocused sends no keyup.
25
+ */
26
+ export function createPanModeHandlers(setActive, reroutePress) {
27
+ let active = false;
28
+ const set = (next) => {
29
+ if (next === active)
30
+ return;
31
+ active = next;
32
+ setActive(next);
33
+ reroutePress();
34
+ };
19
35
  const handleKeyDown = (event) => {
20
- if (event.code === 'Space') {
21
- setActive(true);
22
- }
36
+ if (event.code === 'Space')
37
+ set(true);
23
38
  };
24
39
  const handleKeyUp = (event) => {
25
- if (event.code === 'Space') {
26
- setActive(false);
27
- }
40
+ if (event.code === 'Space')
41
+ set(false);
42
+ };
43
+ return { handleKeyDown, handleKeyUp, handleBlur: () => set(false) };
44
+ }
45
+ /**
46
+ * The brush handle under a press, read from the overlay's DOM: its element's
47
+ * `data-brush-handle`, and the `data-brush-owner` of its overlay (a parallel
48
+ * axis's column), or null when the press is not on a brush.
49
+ */
50
+ export function brushHandleAt(target) {
51
+ const el = target?.closest?.('[data-brush-handle]');
52
+ if (!el)
53
+ return null;
54
+ return {
55
+ handle: el.getAttribute('data-brush-handle'),
56
+ owner: el.closest('[data-brush-owner]')?.getAttribute('data-brush-owner') ?? null
28
57
  };
29
- return { handleKeyDown, handleKeyUp };
30
58
  }
31
59
  export function scaleBrushExtent1D(min, max, scaleFactor) {
32
60
  const center = (min + max) / 2;
@@ -5,13 +5,23 @@
5
5
  * import { setQueryContext } from './';
6
6
  * setQueryContext(wasm.getConnection()); // Promise OK — resolves lazily
7
7
  *
8
- * Usage in spec renderer:
9
- * context.set('query-engine', new QueryEngine(connection));
10
- * context.set('filter-groups', new FilterGroupRegistry());
8
+ * Usage outside a component (the spec renderer), passing contexts to mount():
9
+ * const { filterGroups } = createQueryServices(queryEngine);
10
+ * context.set('query-engine', queryEngine);
11
+ * context.set('filter-groups', filterGroups);
11
12
  */
12
13
  import { QueryEngine } from './QueryEngine';
13
14
  import type { DuckDBConnection } from './QueryEngine';
14
15
  import { FilterGroupRegistry } from './FilterGroupRegistry.svelte';
16
+ /**
17
+ * Create the FilterGroupRegistry for an engine and wire both to the schema
18
+ * service. Brushes and toggles reach the engine only through this wiring:
19
+ * a FilterGroup without an engine updates its selection but never re-queries.
20
+ */
21
+ export declare function createQueryServices(queryEngine: QueryEngine): {
22
+ queryEngine: QueryEngine;
23
+ filterGroups: FilterGroupRegistry;
24
+ };
15
25
  /**
16
26
  * Create QueryEngine + FilterGroupRegistry and set Svelte contexts.
17
27
  * Call this synchronously in a component's <script> block.