@pythia-software/query-table-ui 0.1.0 → 0.3.0

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.
package/dist/index.js CHANGED
@@ -137,16 +137,18 @@ function formatRelative(iso) {
137
137
  }
138
138
 
139
139
  // src/DataTable.tsx
140
- import { useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
140
+ import { isComputedCellError } from "@pythia-software/query-table-core";
141
+ import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
142
+ import { useVirtualizer } from "@tanstack/react-virtual";
141
143
  import { isSortable, readFieldValue } from "@pythia-software/query-table-core";
142
144
  import { useColumnDrag } from "@pythia-software/query-table-react";
143
145
 
144
146
  // src/CellMenu.tsx
145
147
  import { useEffect } from "react";
146
- import { NULLARY_OPS, opsForField } from "@pythia-software/query-table-core";
148
+ import { NULLARY_OPS, negateClause, opsForField, opPairsForField, isFilterable } from "@pythia-software/query-table-core";
147
149
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
148
150
  var cx = (...parts) => parts.filter(Boolean).join(" ");
149
- var MENU_WIDTH = 240;
151
+ var MENU_WIDTH = 320;
150
152
  var ROW_HEIGHT = 32;
151
153
  var POINTER_OFFSET = 4;
152
154
  function CellMenu({ field, value, x, y, onAddFilter, onClose, classNames }) {
@@ -165,73 +167,36 @@ function CellMenu({ field, value, x, y, onAddFilter, onClose, classNames }) {
165
167
  clearTimeout(t);
166
168
  };
167
169
  }, [onClose]);
168
- const ops = opsForField(field);
170
+ const ops = isFilterable(field) ? opsForField(field) : [];
171
+ const pairs = isFilterable(field) ? opPairsForField(field) : [];
172
+ const hasNullity = pairs.some((p) => p.keep.op === "is_null");
169
173
  const isArray = Array.isArray(value);
170
174
  const isNullish = value == null || value === "" || isArray && value.length === 0;
171
- function apply(op, raw) {
172
- const v = raw != null ? raw : valueToString(value);
173
- onAddFilter({ field: field.name, op, value: NULLARY_OPS.has(op) ? "" : v });
175
+ const cellText = valueToString(value);
176
+ function applyClause(clause) {
177
+ onAddFilter(clause);
174
178
  onClose();
175
179
  }
176
- const filterItems = [];
180
+ const rows = [];
181
+ const pairRow = (op, raw) => {
182
+ const positive = { field: field.name, op, value: NULLARY_OPS.has(op) ? "" : raw };
183
+ rows.push({ positive, negative: negateClause(positive, ops) });
184
+ };
177
185
  if (isArray && !isNullish) {
178
- value.forEach((el, i) => {
179
- const s = String(el);
180
- const arrayOp = ops.includes("includes") ? "includes" : ops[0] ?? "=";
181
- filterItems.push(
182
- /* @__PURE__ */ jsxs(MenuItem, { classNames, onClick: () => apply(arrayOp, s), children: [
183
- "filter: ",
184
- opSymbol(arrayOp),
185
- " ",
186
- /* @__PURE__ */ jsx("code", { children: s })
187
- ] }, `el-${i}`)
188
- );
189
- });
190
- }
191
- for (const op of ops) {
192
- const nullary = NULLARY_OPS.has(op);
193
- if (isNullish && !nullary) continue;
194
- if (isArray && !nullary && !isNullish) continue;
195
- filterItems.push(
196
- /* @__PURE__ */ jsxs(MenuItem, { classNames, onClick: () => apply(op), children: [
197
- "filter: ",
198
- opSymbol(op),
199
- !nullary && !isNullish ? /* @__PURE__ */ jsxs(Fragment, { children: [
200
- " ",
201
- /* @__PURE__ */ jsx("code", { children: previewValue(value) })
202
- ] }) : null
203
- ] }, op)
204
- );
186
+ const arrayOp = pairs.find((p) => p.keep.op === "includes")?.keep.op ?? pairs[0]?.keep.op ?? "=";
187
+ value.forEach((el) => pairRow(arrayOp, String(el)));
188
+ if (hasNullity) pairRow("is_null", cellText);
189
+ } else if (!isNullish) {
190
+ for (const pair of pairs) pairRow(pair.keep.op, cellText);
191
+ } else if (hasNullity) {
192
+ pairRow("is_null", cellText);
205
193
  }
206
- const copyItem = /* @__PURE__ */ jsx(
207
- MenuItem,
208
- {
209
- classNames,
210
- onClick: async () => {
211
- try {
212
- await navigator.clipboard.writeText(valueToString(value));
213
- } catch {
214
- }
215
- onClose();
216
- },
217
- children: "copy value"
218
- },
219
- "copy"
220
- );
221
- const items = [
222
- /* @__PURE__ */ jsxs("div", { className: "qt-cm-header", children: [
223
- /* @__PURE__ */ jsx("span", { className: "qt-cm-field", children: field.label }),
224
- /* @__PURE__ */ jsx("span", { className: "qt-cm-value", children: previewValue(value) })
225
- ] }, "header"),
226
- copyItem,
227
- /* @__PURE__ */ jsx("div", { className: cx("qt-cm-sep", classNames?.separator) }, "sep"),
228
- ...filterItems
229
- ];
230
194
  const vw = typeof window !== "undefined" ? window.innerWidth : 9999;
231
195
  const vh = typeof window !== "undefined" ? window.innerHeight : 9999;
196
+ const approxRows = 3 + rows.length;
232
197
  const left = Math.max(8, Math.min(x + POINTER_OFFSET, vw - MENU_WIDTH - 8));
233
- const top = Math.max(8, Math.min(y + POINTER_OFFSET, vh - ROW_HEIGHT * items.length - 8));
234
- return /* @__PURE__ */ jsx(
198
+ const top = Math.max(8, Math.min(y + POINTER_OFFSET, vh - ROW_HEIGHT * approxRows - 8));
199
+ return /* @__PURE__ */ jsxs(
235
200
  "div",
236
201
  {
237
202
  className: cx("qt-cell-menu", classNames?.popover),
@@ -239,7 +204,69 @@ function CellMenu({ field, value, x, y, onAddFilter, onClose, classNames }) {
239
204
  onClick: (e) => e.stopPropagation(),
240
205
  onContextMenu: (e) => e.preventDefault(),
241
206
  role: "menu",
242
- children: items
207
+ children: [
208
+ /* @__PURE__ */ jsxs("div", { className: "qt-cm-header", children: [
209
+ /* @__PURE__ */ jsx("span", { className: "qt-cm-field", children: field.label }),
210
+ /* @__PURE__ */ jsx("span", { className: "qt-cm-value", children: previewValue(value) })
211
+ ] }),
212
+ /* @__PURE__ */ jsx(
213
+ MenuItem,
214
+ {
215
+ classNames,
216
+ onClick: async () => {
217
+ try {
218
+ await navigator.clipboard.writeText(cellText);
219
+ } catch {
220
+ }
221
+ onClose();
222
+ },
223
+ children: "copy value"
224
+ }
225
+ ),
226
+ /* @__PURE__ */ jsx("div", { className: cx("qt-cm-sep", classNames?.separator) }),
227
+ rows.length > 0 && /* @__PURE__ */ jsxs("div", { className: "qt-cm-filter-grid", role: "group", "aria-label": "Filter by this cell", children: [
228
+ /* @__PURE__ */ jsx("span", { className: "qt-cm-col-head", children: "keep" }),
229
+ /* @__PURE__ */ jsx("span", { className: "qt-cm-col-head qt-cm-col-head--neg", children: "exclude" }),
230
+ rows.map((row, i) => /* @__PURE__ */ jsx(FilterPairRow, { row, value, classNames, onApply: applyClause }, i))
231
+ ] })
232
+ ]
233
+ }
234
+ );
235
+ }
236
+ function FilterPairRow({
237
+ row,
238
+ value,
239
+ classNames,
240
+ onApply
241
+ }) {
242
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
243
+ /* @__PURE__ */ jsx(FilterButton, { clause: row.positive, value, classNames, onApply }),
244
+ /* @__PURE__ */ jsx(FilterButton, { clause: row.negative, value, negative: true, classNames, onApply })
245
+ ] });
246
+ }
247
+ function FilterButton({
248
+ clause,
249
+ value,
250
+ negative,
251
+ classNames,
252
+ onApply
253
+ }) {
254
+ if (!clause) return /* @__PURE__ */ jsx("span", { className: "qt-cm-filter-empty", "aria-hidden": true });
255
+ const nullary = NULLARY_OPS.has(clause.op);
256
+ return /* @__PURE__ */ jsxs(
257
+ "button",
258
+ {
259
+ type: "button",
260
+ className: cx("qt-cm-item", "qt-cm-filter", negative && "qt-cm-filter--neg", classNames?.item),
261
+ onClick: () => onApply(clause),
262
+ role: "menuitem",
263
+ children: [
264
+ /* @__PURE__ */ jsx("span", { className: "qt-cm-op", children: predicateSymbol(clause) }),
265
+ !nullary ? /* @__PURE__ */ jsxs(Fragment, { children: [
266
+ " ",
267
+ /* @__PURE__ */ jsx("code", { children: previewValue(value) })
268
+ ] }) : null
269
+ ]
243
270
  }
244
271
  );
245
272
  }
@@ -250,6 +277,10 @@ function MenuItem({
250
277
  }) {
251
278
  return /* @__PURE__ */ jsx("button", { type: "button", className: cx("qt-cm-item", classNames?.item), onClick, role: "menuitem", children });
252
279
  }
280
+ function predicateSymbol(clause) {
281
+ const base = opSymbol(clause.op);
282
+ return clause.negated ? `not ${base}` : base;
283
+ }
253
284
  function opSymbol(op) {
254
285
  switch (op) {
255
286
  case "!=":
@@ -258,6 +289,14 @@ function opSymbol(op) {
258
289
  return "\u2265";
259
290
  case "<=":
260
291
  return "\u2264";
292
+ case "starts_with":
293
+ return "starts with";
294
+ case "ends_with":
295
+ return "ends with";
296
+ case "matches_regex":
297
+ return "matches";
298
+ case "not_matches_regex":
299
+ return "not matches";
261
300
  case "is_null":
262
301
  return "is null";
263
302
  case "is_not_null":
@@ -292,6 +331,9 @@ var AUTOFIT_EXTRA_PX = 16;
292
331
  var MENU_WIDTH2 = 240;
293
332
  var MENU_ROW_HEIGHT = 32;
294
333
  var COPY_FEEDBACK_MS = 900;
334
+ var DEFAULT_MAX_HEIGHT = 600;
335
+ var DEFAULT_ROW_HEIGHT = 37;
336
+ var DEFAULT_OVERSCAN = 8;
295
337
  var SELECTION_COLUMN = "__qt_selection__";
296
338
  function DataTable(props) {
297
339
  const {
@@ -307,28 +349,76 @@ function DataTable(props) {
307
349
  total = null,
308
350
  loading,
309
351
  emptyMessage,
352
+ maxHeight = DEFAULT_MAX_HEIGHT,
353
+ estimateRowHeight = DEFAULT_ROW_HEIGHT,
354
+ overscan = DEFAULT_OVERSCAN,
310
355
  classNames
311
356
  } = props;
312
357
  const [menu, setMenu] = useState2(null);
313
- const [resizingField, setResizingField] = useState2(null);
314
- const [draftWidths, setDraftWidths] = useState2({});
315
358
  const [selectionColumnWidth, setSelectionColumnWidth] = useState2(DEFAULT_SELECTION_WIDTH);
316
359
  const [tableColumnOrder, setTableColumnOrder] = useState2([]);
317
360
  const [copiedCellKey, setCopiedCellKey] = useState2(null);
361
+ const [headerHeight, setHeaderHeight] = useState2(0);
362
+ const [showHorizontalCue, setShowHorizontalCue] = useState2(false);
318
363
  const tableWrapRef = useRef(null);
364
+ const tableHeadRef = useRef(null);
365
+ const tableRef = useRef(null);
366
+ const resizeGuideRef = useRef(null);
319
367
  const resizeCommitRef = useRef(null);
368
+ const resizePreviewRef = useRef(null);
369
+ const resizeFrameRef = useRef(null);
370
+ const cancelResizeRef = useRef(null);
320
371
  const resizingFieldRef = useRef(null);
321
372
  const copiedCellTimerRef = useRef(null);
322
373
  const localColumnDrag = useColumnDrag();
323
374
  const columnDrag = props.columnDrag ?? localColumnDrag;
375
+ const rowIds = useMemo(() => rows.map(rowId), [rowId, rows]);
376
+ const virtualRowKeys = useMemo(
377
+ () => rowIds.map((id, index) => id == null ? `index:${index}` : `${typeof id}:${String(id)}`),
378
+ [rowIds]
379
+ );
380
+ const getVirtualRowKey = useCallback((index) => virtualRowKeys[index] ?? `index:${index}`, [virtualRowKeys]);
381
+ const normalizedEstimate = Number.isFinite(estimateRowHeight) ? Math.max(1, estimateRowHeight) : DEFAULT_ROW_HEIGHT;
382
+ const normalizedOverscan = Number.isFinite(overscan) ? Math.max(0, Math.floor(overscan)) : DEFAULT_OVERSCAN;
383
+ const estimateVirtualRow = useCallback(() => normalizedEstimate, [normalizedEstimate]);
384
+ const initialViewportHeight = typeof maxHeight === "number" && maxHeight > 0 ? maxHeight : DEFAULT_MAX_HEIGHT;
385
+ const initialRect = useMemo(() => ({ width: 0, height: initialViewportHeight }), [initialViewportHeight]);
386
+ const rowVirtualizer = useVirtualizer({
387
+ count: rows.length,
388
+ getScrollElement: () => tableWrapRef.current,
389
+ estimateSize: estimateVirtualRow,
390
+ getItemKey: getVirtualRowKey,
391
+ overscan: normalizedOverscan,
392
+ // The header participates in normal table layout even while it is sticky.
393
+ // Offset row measurements so the virtual range matches the body's origin.
394
+ scrollMargin: headerHeight,
395
+ initialRect
396
+ });
324
397
  useEffect2(() => {
325
398
  return () => {
326
399
  if (copiedCellTimerRef.current) {
327
400
  clearTimeout(copiedCellTimerRef.current);
328
401
  copiedCellTimerRef.current = null;
329
402
  }
403
+ cancelResizeRef.current?.();
330
404
  };
331
405
  }, []);
406
+ useEffect2(() => {
407
+ const header = tableHeadRef.current;
408
+ if (!header) return;
409
+ const measureHeader = () => {
410
+ const next = header.getBoundingClientRect().height;
411
+ setHeaderHeight((current) => current === next ? current : next);
412
+ };
413
+ measureHeader();
414
+ if (typeof ResizeObserver === "undefined") {
415
+ window.addEventListener("resize", measureHeader);
416
+ return () => window.removeEventListener("resize", measureHeader);
417
+ }
418
+ const observer = new ResizeObserver(measureHeader);
419
+ observer.observe(header);
420
+ return () => observer.disconnect();
421
+ }, []);
332
422
  function cellKey(rowIdValue, rowIndex, fieldName) {
333
423
  return rowIdValue == null ? `row:${rowIndex}:${fieldName}` : `id:${rowIdValue}:${fieldName}`;
334
424
  }
@@ -361,7 +451,7 @@ function DataTable(props) {
361
451
  }
362
452
  showCopyFeedback(cellKey(rowIdValue, rowIndex, f.name));
363
453
  }
364
- const pageIds = rows.map(rowId).filter((id) => id != null);
454
+ const pageIds = rowIds.filter((id) => id != null);
365
455
  const headerState = selection ? selection.pageState(pageIds) : "none";
366
456
  const showSel = selection != null;
367
457
  function sortKeyFor(f) {
@@ -396,11 +486,11 @@ function DataTable(props) {
396
486
  return col?.width ?? f.select?.width;
397
487
  }
398
488
  function resolvedWidthFor(name, f) {
399
- return draftWidths[name] ?? widthFor(name, f);
489
+ return widthFor(name, f);
400
490
  }
401
491
  function resolvedColumnWidthFor(name, f) {
402
- if (name === SELECTION_COLUMN) return draftWidths[name] ?? selectionColumnWidth;
403
- return f ? resolvedWidthFor(name, f) : draftWidths[name];
492
+ if (name === SELECTION_COLUMN) return selectionColumnWidth;
493
+ return f ? resolvedWidthFor(name, f) : void 0;
404
494
  }
405
495
  function displayColumnWidthFor(name, f) {
406
496
  if (name === SELECTION_COLUMN) return resolvedColumnWidthFor(name) ?? DEFAULT_SELECTION_WIDTH;
@@ -424,16 +514,20 @@ function DataTable(props) {
424
514
  const px = clampWidth(width);
425
515
  onQueryChange((prev) => ({ ...prev, select: writeWidth(prev.select, fields, name, px) }));
426
516
  }
427
- function setDraftWidth(name, width) {
428
- setDraftWidths((prev) => prev[name] === width ? prev : { ...prev, [name]: width });
429
- }
430
- function clearDraftWidth(name) {
431
- setDraftWidths((prev) => {
432
- if (!(name in prev)) return prev;
433
- const next = { ...prev };
434
- delete next[name];
435
- return next;
436
- });
517
+ function applyResizeGuide() {
518
+ const preview = resizePreviewRef.current;
519
+ const guide = resizeGuideRef.current;
520
+ if (!preview || !guide) return;
521
+ guide.style.transform = `translateX(${preview.guideX}px)`;
522
+ }
523
+ function applyCommittedResize() {
524
+ const preview = resizePreviewRef.current;
525
+ const table = tableRef.current;
526
+ if (!preview || !table) return;
527
+ const selector = `col[data-qt-column="${cssAttributeValue(preview.name)}"]`;
528
+ const col = table.querySelector(selector);
529
+ if (col) col.style.width = `${preview.width}px`;
530
+ table.style.minWidth = `${preview.startTableWidth + preview.width - preview.previousWidth}px`;
437
531
  }
438
532
  function startResize(name, startWidth, e) {
439
533
  e.preventDefault();
@@ -442,30 +536,70 @@ function DataTable(props) {
442
536
  const startW = startWidth ?? e.currentTarget.parentElement?.getBoundingClientRect().width ?? DEFAULT_WIDTH;
443
537
  const previousCursor = document.body.style.cursor;
444
538
  const previousUserSelect = document.body.style.userSelect;
539
+ const initialWidth = clampColumnWidth(name, startW);
540
+ const wrap = tableWrapRef.current;
541
+ const wrapRect = wrap?.getBoundingClientRect();
542
+ const header = e.currentTarget.parentElement;
445
543
  resizeCommitRef.current = null;
544
+ resizePreviewRef.current = {
545
+ name,
546
+ width: initialWidth,
547
+ previousWidth: displayColumnWidthFor(name, fieldByName.get(name)),
548
+ startTableWidth: tableWidth,
549
+ guideX: startX - (wrapRect?.left ?? 0) + (wrap?.scrollLeft ?? 0)
550
+ };
446
551
  resizingFieldRef.current = name;
447
- setResizingField(name);
448
- setDraftWidth(name, clampColumnWidth(name, startW));
552
+ tableWrapRef.current?.classList.add("qt-table-wrap--resizing");
553
+ header?.classList.add("qt-th--resizing");
554
+ if (header) header.draggable = false;
555
+ if (resizeGuideRef.current) {
556
+ resizeGuideRef.current.style.display = "block";
557
+ resizeGuideRef.current.style.top = `${wrap?.scrollTop ?? 0}px`;
558
+ resizeGuideRef.current.style.height = `${wrap?.clientHeight ?? 0}px`;
559
+ }
560
+ applyResizeGuide();
449
561
  document.body.style.cursor = "col-resize";
450
562
  document.body.style.userSelect = "none";
451
- const finish = () => {
563
+ const cleanup = (commitWidth) => {
452
564
  window.removeEventListener("pointermove", onMove);
453
565
  window.removeEventListener("pointerup", finish);
454
566
  window.removeEventListener("pointercancel", finish);
567
+ if (resizeFrameRef.current != null) {
568
+ cancelAnimationFrame(resizeFrameRef.current);
569
+ resizeFrameRef.current = null;
570
+ applyResizeGuide();
571
+ }
455
572
  document.body.style.cursor = previousCursor;
456
573
  document.body.style.userSelect = previousUserSelect;
457
- const commit = resizeCommitRef.current;
574
+ tableWrapRef.current?.classList.remove("qt-table-wrap--resizing");
575
+ tableRef.current?.querySelector(`th[data-qt-field="${cssAttributeValue(name)}"]`)?.classList.remove("qt-th--resizing");
576
+ if (header) header.draggable = true;
577
+ const commit = commitWidth ? resizeCommitRef.current : null;
578
+ if (commit) applyCommittedResize();
579
+ if (resizeGuideRef.current) resizeGuideRef.current.style.display = "none";
458
580
  resizeCommitRef.current = null;
581
+ resizePreviewRef.current = null;
459
582
  resizingFieldRef.current = null;
460
- setResizingField(null);
461
- clearDraftWidth(name);
583
+ cancelResizeRef.current = null;
462
584
  if (commit) commitColumnWidth(commit.name, commit.width);
463
585
  };
586
+ const finish = () => cleanup(true);
464
587
  const onMove = (ev) => {
465
588
  const next = clampColumnWidth(name, startW + (ev.clientX - startX));
466
589
  resizeCommitRef.current = { name, width: next };
467
- setDraftWidth(name, next);
590
+ const preview = resizePreviewRef.current;
591
+ if (preview) {
592
+ preview.width = next;
593
+ preview.guideX = startX + (next - startW) - (wrapRect?.left ?? 0) + (wrap?.scrollLeft ?? 0);
594
+ }
595
+ if (resizeFrameRef.current == null) {
596
+ resizeFrameRef.current = requestAnimationFrame(() => {
597
+ resizeFrameRef.current = null;
598
+ applyResizeGuide();
599
+ });
600
+ }
468
601
  };
602
+ cancelResizeRef.current = () => cleanup(false);
469
603
  window.addEventListener("pointermove", onMove);
470
604
  window.addEventListener("pointerup", finish);
471
605
  window.addEventListener("pointercancel", finish);
@@ -573,6 +707,22 @@ function DataTable(props) {
573
707
  const f = fieldByName.get(name);
574
708
  return sum + displayColumnWidthFor(name, f);
575
709
  }, trailing ? 40 : 0);
710
+ useEffect2(() => {
711
+ const wrap = tableWrapRef.current;
712
+ if (!wrap) return;
713
+ const updateCue = () => {
714
+ const hasMoreToRight = wrap.scrollWidth - wrap.clientWidth - wrap.scrollLeft > 2;
715
+ setShowHorizontalCue(hasMoreToRight);
716
+ };
717
+ updateCue();
718
+ window.addEventListener("resize", updateCue);
719
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(updateCue);
720
+ observer?.observe(wrap);
721
+ return () => {
722
+ window.removeEventListener("resize", updateCue);
723
+ observer?.disconnect();
724
+ };
725
+ }, [renderedColumnNames, tableWidth]);
576
726
  const start = query.offset;
577
727
  const end = total != null ? Math.min(start + query.limit, total) : start + rows.length;
578
728
  const canPrev = start > 0;
@@ -580,7 +730,16 @@ function DataTable(props) {
580
730
  const prevPage = () => onQueryChange((prev) => ({ ...prev, offset: Math.max(0, prev.offset - prev.limit) }));
581
731
  const nextPage = () => onQueryChange((prev) => ({ ...prev, offset: prev.offset + prev.limit }));
582
732
  const summaryText = total == null || total === 0 ? `${rows.length} rows` : `${start + 1}\u2013${end} of ${total}`;
733
+ const virtualRows = rowVirtualizer.getVirtualItems();
734
+ const firstVirtualRow = virtualRows[0];
735
+ const lastVirtualRow = virtualRows[virtualRows.length - 1];
736
+ const paddingTop = firstVirtualRow ? Math.max(0, firstVirtualRow.start - headerHeight) : 0;
737
+ const paddingBottom = lastVirtualRow ? Math.max(0, rowVirtualizer.getTotalSize() - (lastVirtualRow.end - headerHeight)) : 0;
583
738
  return /* @__PURE__ */ jsxs2(Fragment2, { children: [
739
+ showHorizontalCue ? /* @__PURE__ */ jsxs2("div", { className: "qt-table-scroll-cue", "aria-hidden": "true", children: [
740
+ "Swipe to see more ",
741
+ /* @__PURE__ */ jsx2("span", { children: "\u2192" })
742
+ ] }) : null,
584
743
  /* @__PURE__ */ jsxs2(
585
744
  "div",
586
745
  {
@@ -588,24 +747,40 @@ function DataTable(props) {
588
747
  className: cx2(
589
748
  "qt-table-wrap",
590
749
  loading && "qt-table-wrap--loading",
591
- !!resizingField && "qt-table-wrap--resizing",
592
750
  classNames?.wrap
593
751
  ),
594
752
  onWheel: onTableWheel,
753
+ style: { maxHeight, overflowY: "auto" },
754
+ onScroll: () => {
755
+ const wrap = tableWrapRef.current;
756
+ if (!wrap) return;
757
+ setShowHorizontalCue(wrap.scrollWidth - wrap.clientWidth - wrap.scrollLeft > 2);
758
+ },
759
+ role: "region",
760
+ "aria-label": "Scrollable data table",
761
+ tabIndex: 0,
595
762
  children: [
596
763
  loading && /* @__PURE__ */ jsx2("div", { className: cx2("qt-loading-bar", classNames?.loadingBar), role: "progressbar", "aria-label": "loading" }),
597
- /* @__PURE__ */ jsxs2("table", { className: cx2("qt-table", classNames?.table), style: { minWidth: tableWidth }, children: [
764
+ /* @__PURE__ */ jsx2(
765
+ "div",
766
+ {
767
+ ref: resizeGuideRef,
768
+ className: cx2("qt-resize-guide", classNames?.resizeGuide),
769
+ "aria-hidden": "true"
770
+ }
771
+ ),
772
+ /* @__PURE__ */ jsxs2("table", { ref: tableRef, className: cx2("qt-table", classNames?.table), style: { minWidth: tableWidth }, children: [
598
773
  /* @__PURE__ */ jsxs2("colgroup", { children: [
599
774
  renderedColumnNames.map((name) => {
600
775
  const f = fieldByName.get(name);
601
776
  const w = displayColumnWidthFor(name, f);
602
- if (name === SELECTION_COLUMN) return /* @__PURE__ */ jsx2("col", { style: { width: w } }, name);
777
+ if (name === SELECTION_COLUMN) return /* @__PURE__ */ jsx2("col", { "data-qt-column": name, style: { width: w } }, name);
603
778
  if (!f) return null;
604
- return /* @__PURE__ */ jsx2("col", { style: { width: w } }, f.name);
779
+ return /* @__PURE__ */ jsx2("col", { "data-qt-column": f.name, style: { width: w } }, f.name);
605
780
  }),
606
781
  trailing && /* @__PURE__ */ jsx2("col", { style: { width: 40 } })
607
782
  ] }),
608
- /* @__PURE__ */ jsx2("thead", { className: classNames?.thead, children: /* @__PURE__ */ jsxs2("tr", { className: classNames?.headerRow, children: [
783
+ /* @__PURE__ */ jsx2("thead", { ref: tableHeadRef, className: classNames?.thead, children: /* @__PURE__ */ jsxs2("tr", { className: classNames?.headerRow, children: [
609
784
  renderedColumnNames.map((name) => {
610
785
  if (name === SELECTION_COLUMN) {
611
786
  return /* @__PURE__ */ jsxs2(
@@ -616,11 +791,10 @@ function DataTable(props) {
616
791
  "qt-th",
617
792
  "qt-checkbox-cell",
618
793
  dragSource === name && "qt-th--dragging",
619
- resizingField === name && "qt-th--resizing",
620
794
  classNames?.th,
621
795
  classNames?.checkboxCell
622
796
  ),
623
- draggable: resizingField == null,
797
+ draggable: true,
624
798
  onDragStart: (e) => {
625
799
  if (resizingFieldRef.current) {
626
800
  e.preventDefault();
@@ -683,11 +857,10 @@ function DataTable(props) {
683
857
  "qt-th",
684
858
  sortable && "qt-th--sortable",
685
859
  dragSource === f.name && "qt-th--dragging",
686
- resizingField === f.name && "qt-th--resizing",
687
860
  classNames?.th
688
861
  ),
689
862
  style: f.select?.align ? { textAlign: f.select.align } : void 0,
690
- draggable: resizingField == null,
863
+ draggable: true,
691
864
  onDragStart: (e) => {
692
865
  if (resizingFieldRef.current) {
693
866
  e.preventDefault();
@@ -746,12 +919,17 @@ function DataTable(props) {
746
919
  ] }) }),
747
920
  /* @__PURE__ */ jsxs2("tbody", { className: classNames?.tbody, children: [
748
921
  rows.length === 0 && /* @__PURE__ */ jsx2("tr", { children: /* @__PURE__ */ jsx2("td", { colSpan: totalCols, className: cx2("qt-cell", "qt-empty", classNames?.empty), children: emptyMessage ?? "No rows." }) }),
749
- rows.map((row, i) => {
750
- const id = rowId(row);
922
+ paddingTop > 0 && /* @__PURE__ */ jsx2("tr", { className: "qt-virtual-spacer", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("td", { colSpan: totalCols, style: { height: paddingTop, padding: 0, border: 0 } }) }),
923
+ virtualRows.map((virtualRow) => {
924
+ const i = virtualRow.index;
925
+ const row = rows[i];
926
+ const id = rowIds[i] ?? null;
751
927
  const selected = id != null && selection ? selection.isSelected(id) : false;
752
928
  return /* @__PURE__ */ jsxs2(
753
929
  "tr",
754
930
  {
931
+ "data-index": i,
932
+ ref: rowVirtualizer.measureElement,
755
933
  className: cx2("qt-row", selected && "qt-row--selected", classNames?.row, selected && classNames?.rowSelected),
756
934
  children: [
757
935
  renderedColumnNames.map((name) => {
@@ -796,7 +974,7 @@ function DataTable(props) {
796
974
  onClick: (e) => openMenu(e, f, row),
797
975
  onContextMenu: (e) => openMenu(e, f, row),
798
976
  onDoubleClick: (e) => copyCellToClipboard(e, f, id, i, value),
799
- children: copiedCellKey === key ? /* @__PURE__ */ jsx2("span", { className: "qt-cell-copy-chip", children: "\u2713 copied" }) : render({ value, row, field: f, query })
977
+ children: copiedCellKey === key ? /* @__PURE__ */ jsx2("span", { className: "qt-cell-copy-chip", children: "\u2713 copied" }) : isComputedCellError(value) ? /* @__PURE__ */ jsx2("span", { className: "qt-formula-error", title: value.computedError, children: value.computedError === "Calculating\u2026" ? "Calculating\u2026" : "Error" }) : render({ value, row, field: f, query })
800
978
  },
801
979
  f.name
802
980
  );
@@ -804,9 +982,10 @@ function DataTable(props) {
804
982
  trailing && /* @__PURE__ */ jsx2("td", { className: cx2("qt-cell", "qt-trailing-cell", classNames?.cell), children: trailing(row) })
805
983
  ]
806
984
  },
807
- id != null ? String(id) : i
985
+ virtualRow.key
808
986
  );
809
- })
987
+ }),
988
+ paddingBottom > 0 && /* @__PURE__ */ jsx2("tr", { className: "qt-virtual-spacer", "aria-hidden": "true", children: /* @__PURE__ */ jsx2("td", { colSpan: totalCols, style: { height: paddingBottom, padding: 0, border: 0 } }) })
810
989
  ] }),
811
990
  /* @__PURE__ */ jsx2("tfoot", { children: /* @__PURE__ */ jsx2("tr", { className: cx2("qt-table-summary-row", classNames?.summaryRow), children: /* @__PURE__ */ jsx2("td", { colSpan: totalCols, className: cx2("qt-cell", "qt-table-summary-cell", classNames?.summaryCell), children: /* @__PURE__ */ jsxs2("div", { className: cx2("qt-table-summary", classNames?.summaryControls), children: [
812
991
  /* @__PURE__ */ jsx2(
@@ -973,25 +1152,804 @@ function valueToString2(value) {
973
1152
  return String(value);
974
1153
  }
975
1154
 
1155
+ // src/SelectColumnEditor.tsx
1156
+ import {
1157
+ lazy,
1158
+ Suspense,
1159
+ useEffect as useEffect3,
1160
+ useId,
1161
+ useMemo as useMemo2,
1162
+ useRef as useRef2,
1163
+ useState as useState3
1164
+ } from "react";
1165
+ import {
1166
+ computedFieldName,
1167
+ FORMULA_FUNCTIONS,
1168
+ isComputedField,
1169
+ isSelectable
1170
+ } from "@pythia-software/query-table-core";
1171
+
1172
+ // src/previewOrdering.ts
1173
+ var collator = new Intl.Collator(void 0, {
1174
+ numeric: true,
1175
+ sensitivity: "base"
1176
+ });
1177
+ var valueLabel = (value) => value === null ? "NULL" : typeof value === "string" ? value : JSON.stringify(value);
1178
+ function mixedRank(group) {
1179
+ const value = JSON.stringify([
1180
+ group.inputs,
1181
+ group.result.value,
1182
+ group.result.error ?? null
1183
+ ]);
1184
+ let hash = 2166136261;
1185
+ for (let i = 0; i < value.length; i++)
1186
+ hash = Math.imul(hash ^ value.charCodeAt(i), 16777619);
1187
+ return hash >>> 0;
1188
+ }
1189
+ function compareValue(a, b) {
1190
+ return collator.compare(valueLabel(a), valueLabel(b));
1191
+ }
1192
+ function sortValue(group, key) {
1193
+ switch (key.kind) {
1194
+ case "input":
1195
+ return group.inputs[key.index] ?? null;
1196
+ case "result":
1197
+ return group.result.error ?? group.result.value;
1198
+ case "count":
1199
+ case "percentage":
1200
+ return group.count;
1201
+ }
1202
+ }
1203
+ function orderPreviewGroups(groups, sort) {
1204
+ return groups.map((group, index) => ({ group, index })).sort((a, b) => {
1205
+ if (!sort) {
1206
+ const rank = mixedRank(a.group) - mixedRank(b.group);
1207
+ return rank || a.index - b.index;
1208
+ }
1209
+ const comparison = compareValue(
1210
+ sortValue(a.group, sort.key),
1211
+ sortValue(b.group, sort.key)
1212
+ );
1213
+ return (sort.direction === "asc" ? comparison : -comparison) || a.index - b.index;
1214
+ }).map(({ group }) => group);
1215
+ }
1216
+
1217
+ // src/SelectColumnEditor.tsx
1218
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1219
+ var FormulaEditor = lazy(() => import("./FormulaEditor-YCPICDIH.js"));
1220
+ var fieldExpression = (name) => `[${name.replace(/\]/g, "]]")}]`;
1221
+ var display = (v) => v === null ? "NULL" : typeof v === "string" ? v === "" ? '""' : v : JSON.stringify(v);
1222
+ function SelectColumnEditor({
1223
+ api,
1224
+ onClose
1225
+ }) {
1226
+ const titleId = useId(), dialog = useRef2(null);
1227
+ const closeRef = useRef2(onClose);
1228
+ closeRef.current = onClose;
1229
+ const [columns, setColumns] = useState3(
1230
+ () => api.select.visible.map((c) => ({ ...c }))
1231
+ );
1232
+ const [search, setSearch] = useState3(""), [active, setActive] = useState3(api.select.visible[0]?.field ?? "");
1233
+ const [editing, setEditing] = useState3(false), [id, setId] = useState3(""), [revision, setRevision] = useState3(null);
1234
+ const [label, setLabel] = useState3(""), [source, setSource] = useState3("");
1235
+ const [count, setCount] = useState3(1e3), [preview, setPreview] = useState3(null), [loading, setLoading] = useState3(false);
1236
+ const [error, setError] = useState3(null), [notice, setNotice] = useState3(""), [saving, setSaving] = useState3(false);
1237
+ const [filter, setFilter] = useState3("all"), [page, setPage] = useState3(0), [retry, setRetry] = useState3(0), [functionSearch, setFunctionSearch] = useState3("");
1238
+ const [previewSort, setPreviewSort] = useState3(null);
1239
+ const drag = useRef2(null);
1240
+ const [inspection, setInspection] = useState3(null);
1241
+ const catalogue = api.computed.catalogue.filter(isSelectable);
1242
+ const chosen = catalogue.find((f) => f.name === active);
1243
+ const expression = editing ? source : active ? fieldExpression(active) : "";
1244
+ const validation = useMemo2(() => {
1245
+ if (!expression)
1246
+ return { error: "Choose a column or create a computed column." };
1247
+ try {
1248
+ return {
1249
+ plan: api.computed.compile(expression, editing ? id : void 0)
1250
+ };
1251
+ } catch (e) {
1252
+ return { error: e instanceof Error ? e.message : String(e) };
1253
+ }
1254
+ }, [expression, editing, id, api.computed.compile]);
1255
+ useEffect3(() => {
1256
+ const previous = document.activeElement;
1257
+ dialog.current?.focus();
1258
+ const onKey = (e) => {
1259
+ if (e.key === "Escape") {
1260
+ if (document.querySelector(".cm-tooltip-autocomplete")) return;
1261
+ closeRef.current();
1262
+ }
1263
+ if (e.key === "Tab") {
1264
+ const focusable = Array.from(
1265
+ dialog.current?.querySelectorAll(
1266
+ 'button:not([disabled]), input:not([disabled]), select, textarea, [tabindex="0"], [contenteditable="true"]'
1267
+ ) ?? []
1268
+ ).filter((el) => el.getClientRects().length);
1269
+ const first = focusable[0], last = focusable[focusable.length - 1];
1270
+ if (e.shiftKey && (document.activeElement === first || document.activeElement === dialog.current)) {
1271
+ e.preventDefault();
1272
+ last?.focus();
1273
+ } else if (!e.shiftKey && document.activeElement === last) {
1274
+ e.preventDefault();
1275
+ first?.focus();
1276
+ }
1277
+ }
1278
+ };
1279
+ document.addEventListener("keydown", onKey);
1280
+ const old = document.body.style.overflow;
1281
+ document.body.style.overflow = "hidden";
1282
+ return () => {
1283
+ document.removeEventListener("keydown", onKey);
1284
+ document.body.style.overflow = old;
1285
+ previous?.focus();
1286
+ };
1287
+ }, []);
1288
+ useEffect3(() => {
1289
+ const ac = new AbortController();
1290
+ setPreview(null);
1291
+ setError(null);
1292
+ setPage(0);
1293
+ setInspection(null);
1294
+ if (validation.error || !Number.isInteger(count) || count < 1 || count > 1e4) {
1295
+ setLoading(false);
1296
+ return;
1297
+ }
1298
+ setLoading(true);
1299
+ const timer = setTimeout(() => {
1300
+ void api.computed.preview(
1301
+ expression,
1302
+ count,
1303
+ ac.signal,
1304
+ editing ? id : void 0,
1305
+ editing
1306
+ ).then((result) => {
1307
+ if (!ac.signal.aborted) setPreview(result);
1308
+ }).catch((e) => {
1309
+ if (!ac.signal.aborted)
1310
+ setError(e instanceof Error ? e.message : String(e));
1311
+ }).finally(() => {
1312
+ if (!ac.signal.aborted) setLoading(false);
1313
+ });
1314
+ }, 400);
1315
+ return () => {
1316
+ ac.abort();
1317
+ clearTimeout(timer);
1318
+ };
1319
+ }, [expression, count, editing, id, validation, api.computed.preview, retry]);
1320
+ const move = (field, index) => setColumns((cols) => {
1321
+ const next = cols.filter((c) => c.field !== field), item = cols.find((c) => c.field === field);
1322
+ if (item) next.splice(Math.max(0, Math.min(index, next.length)), 0, item);
1323
+ return next;
1324
+ });
1325
+ const toggle = (field) => setColumns(
1326
+ (cols) => cols.some((c) => c.field === field) ? cols.filter((c) => c.field !== field) : [...cols, { field }]
1327
+ );
1328
+ function browse(field) {
1329
+ setActive(field);
1330
+ setEditing(false);
1331
+ setNotice("");
1332
+ }
1333
+ function edit(defId) {
1334
+ const def = api.computed.definitions.find((d) => d.id === defId);
1335
+ setId(def?.id ?? defId ?? crypto.randomUUID());
1336
+ setRevision(def?.revision ?? null);
1337
+ setLabel(def?.label ?? defId ?? "");
1338
+ setSource(
1339
+ def?.expression.source ?? (defId ? "" : active ? fieldExpression(active) : "LEFT([field], 3)")
1340
+ );
1341
+ setEditing(true);
1342
+ setNotice("");
1343
+ }
1344
+ async function save() {
1345
+ setSaving(true);
1346
+ setError(null);
1347
+ try {
1348
+ const def = await api.computed.save(
1349
+ {
1350
+ id,
1351
+ label: label.trim(),
1352
+ expression: { language: "qt-expr", version: 1, source }
1353
+ },
1354
+ revision
1355
+ );
1356
+ setRevision(def.revision);
1357
+ const field = computedFieldName(def.id);
1358
+ setActive(field);
1359
+ setColumns(
1360
+ (cols) => cols.some((c) => c.field === field) ? cols : [...cols, { field }]
1361
+ );
1362
+ setNotice(
1363
+ "Definition saved. All queries referencing this ID now use this definition. Apply columns to update this query\u2019s layout."
1364
+ );
1365
+ } catch (e) {
1366
+ setError(e instanceof Error ? e.message : String(e));
1367
+ } finally {
1368
+ setSaving(false);
1369
+ }
1370
+ }
1371
+ const groups = useMemo2(
1372
+ () => orderPreviewGroups(
1373
+ (preview?.groups ?? []).filter(
1374
+ (g) => filter === "errors" ? !!g.result.error : filter === "nulls" ? !g.result.error && g.result.value === null : true
1375
+ ),
1376
+ previewSort
1377
+ ),
1378
+ [preview, filter, previewSort]
1379
+ );
1380
+ const shown = groups.slice(page * 50, (page + 1) * 50);
1381
+ const samePreviewSortKey = (a, b) => {
1382
+ if (a.kind !== b.kind) return false;
1383
+ if (a.kind === "input" && b.kind === "input")
1384
+ return a.index === b.index;
1385
+ return true;
1386
+ };
1387
+ const selectPreviewSort = (key, direction) => {
1388
+ setPreviewSort((current) => {
1389
+ const sameKey = !!current && samePreviewSortKey(current.key, key);
1390
+ return sameKey ? { key, direction: current.direction === "asc" ? "desc" : "asc" } : { key, direction };
1391
+ });
1392
+ setPage(0);
1393
+ };
1394
+ const previewSortDirection = (key) => previewSort && samePreviewSortKey(previewSort.key, key) ? previewSort.direction : void 0;
1395
+ const previewAriaSort = (key) => {
1396
+ const direction = previewSortDirection(key);
1397
+ return direction === "asc" ? "ascending" : direction === "desc" ? "descending" : "none";
1398
+ };
1399
+ const previewSortHeader = (label2, key, direction = "asc") => {
1400
+ const currentDirection = previewSortDirection(key);
1401
+ return /* @__PURE__ */ jsxs3(
1402
+ "button",
1403
+ {
1404
+ type: "button",
1405
+ className: "qt-preview-sort",
1406
+ onClick: () => selectPreviewSort(key, direction),
1407
+ title: `Sort by ${label2}`,
1408
+ children: [
1409
+ label2,
1410
+ currentDirection && (currentDirection === "asc" ? " \u2191" : " \u2193")
1411
+ ]
1412
+ }
1413
+ );
1414
+ };
1415
+ return /* @__PURE__ */ jsx3("div", { className: "qt-modal-backdrop qt-select-editor-backdrop", children: /* @__PURE__ */ jsxs3(
1416
+ "div",
1417
+ {
1418
+ className: "qt-select-editor",
1419
+ role: "dialog",
1420
+ "aria-modal": "true",
1421
+ "aria-labelledby": titleId,
1422
+ ref: dialog,
1423
+ tabIndex: -1,
1424
+ children: [
1425
+ /* @__PURE__ */ jsxs3("header", { className: "qt-select-editor-header", children: [
1426
+ /* @__PURE__ */ jsxs3("div", { children: [
1427
+ /* @__PURE__ */ jsx3("h2", { id: titleId, children: "Select columns" }),
1428
+ /* @__PURE__ */ jsx3("p", { children: "Arrange your view, explore values, and create reusable computed columns." })
1429
+ ] }),
1430
+ /* @__PURE__ */ jsx3(
1431
+ "button",
1432
+ {
1433
+ type: "button",
1434
+ className: "qt-btn",
1435
+ onClick: onClose,
1436
+ "aria-label": "Close column editor",
1437
+ children: "\u2715"
1438
+ }
1439
+ )
1440
+ ] }),
1441
+ /* @__PURE__ */ jsxs3("div", { className: "qt-select-editor-body", children: [
1442
+ /* @__PURE__ */ jsxs3("aside", { className: "qt-selected-panel", children: [
1443
+ /* @__PURE__ */ jsxs3("h3", { children: [
1444
+ "Selected ",
1445
+ /* @__PURE__ */ jsx3("span", { className: "qt-muted", children: columns.length })
1446
+ ] }),
1447
+ /* @__PURE__ */ jsx3("p", { className: "qt-muted", children: "Drag to reorder, or use the arrow buttons." }),
1448
+ /* @__PURE__ */ jsx3("ol", { className: "qt-editor-columns", children: columns.map((c, i) => {
1449
+ const f = catalogue.find((f2) => f2.name === c.field);
1450
+ return /* @__PURE__ */ jsxs3(
1451
+ "li",
1452
+ {
1453
+ draggable: true,
1454
+ onDragStart: (e) => {
1455
+ drag.current = c.field;
1456
+ e.dataTransfer.setData("text/plain", c.field);
1457
+ },
1458
+ onDragOver: (e) => e.preventDefault(),
1459
+ onDrop: (e) => {
1460
+ e.preventDefault();
1461
+ if (drag.current) move(drag.current, i);
1462
+ drag.current = null;
1463
+ },
1464
+ onDragEnd: () => {
1465
+ drag.current = null;
1466
+ },
1467
+ children: [
1468
+ /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "\u283F" }),
1469
+ /* @__PURE__ */ jsx3(
1470
+ "button",
1471
+ {
1472
+ type: "button",
1473
+ className: "qt-column-name",
1474
+ onClick: () => browse(c.field),
1475
+ children: f?.label ?? c.field
1476
+ }
1477
+ ),
1478
+ /* @__PURE__ */ jsx3(
1479
+ "button",
1480
+ {
1481
+ type: "button",
1482
+ className: "qt-icon-btn",
1483
+ disabled: i === 0,
1484
+ onClick: () => move(c.field, i - 1),
1485
+ "aria-label": `Move ${f?.label ?? c.field} up`,
1486
+ children: "\u2191"
1487
+ }
1488
+ ),
1489
+ /* @__PURE__ */ jsx3(
1490
+ "button",
1491
+ {
1492
+ type: "button",
1493
+ className: "qt-icon-btn",
1494
+ disabled: i === columns.length - 1,
1495
+ onClick: () => move(c.field, i + 1),
1496
+ "aria-label": `Move ${f?.label ?? c.field} down`,
1497
+ children: "\u2193"
1498
+ }
1499
+ ),
1500
+ /* @__PURE__ */ jsx3(
1501
+ "button",
1502
+ {
1503
+ type: "button",
1504
+ className: "qt-icon-btn",
1505
+ onClick: () => toggle(c.field),
1506
+ "aria-label": `Remove ${f?.label ?? c.field}`,
1507
+ children: "\xD7"
1508
+ }
1509
+ )
1510
+ ]
1511
+ },
1512
+ c.field
1513
+ );
1514
+ }) }),
1515
+ /* @__PURE__ */ jsx3(
1516
+ "button",
1517
+ {
1518
+ type: "button",
1519
+ className: "qt-link-btn",
1520
+ onClick: () => setColumns(
1521
+ api.defaults.select.length ? api.defaults.select : catalogue.filter((f) => f.select?.default).map((f) => ({ field: f.name }))
1522
+ ),
1523
+ children: "Use default columns"
1524
+ }
1525
+ )
1526
+ ] }),
1527
+ /* @__PURE__ */ jsxs3("aside", { className: "qt-catalogue-panel", children: [
1528
+ /* @__PURE__ */ jsx3("h3", { children: "Column catalogue" }),
1529
+ /* @__PURE__ */ jsx3(
1530
+ "input",
1531
+ {
1532
+ className: "qt-input",
1533
+ "aria-label": "Search columns",
1534
+ placeholder: "Search name or type\u2026",
1535
+ value: search,
1536
+ onChange: (e) => setSearch(e.target.value)
1537
+ }
1538
+ ),
1539
+ /* @__PURE__ */ jsx3(
1540
+ "button",
1541
+ {
1542
+ type: "button",
1543
+ className: "qt-btn qt-create-computed",
1544
+ onClick: () => edit(),
1545
+ children: "\uFF0B Computed column"
1546
+ }
1547
+ ),
1548
+ api.computed.loading && /* @__PURE__ */ jsx3("p", { role: "status", children: "Loading definitions\u2026" }),
1549
+ api.computed.error && /* @__PURE__ */ jsx3("p", { role: "alert", children: api.computed.error }),
1550
+ /* @__PURE__ */ jsx3("div", { className: "qt-catalogue-list", children: catalogue.filter(
1551
+ (f) => `${f.label} ${f.name} ${f.type} ${f.group ?? ""}`.toLowerCase().includes(search.toLowerCase())
1552
+ ).map((f) => /* @__PURE__ */ jsxs3(
1553
+ "div",
1554
+ {
1555
+ className: `qt-catalogue-item ${active === f.name && !editing ? "is-active" : ""}`,
1556
+ children: [
1557
+ /* @__PURE__ */ jsx3(
1558
+ "input",
1559
+ {
1560
+ type: "checkbox",
1561
+ "aria-label": `Include ${f.label}`,
1562
+ checked: columns.some((c) => c.field === f.name),
1563
+ onChange: () => toggle(f.name)
1564
+ }
1565
+ ),
1566
+ /* @__PURE__ */ jsxs3("button", { type: "button", onClick: () => browse(f.name), children: [
1567
+ /* @__PURE__ */ jsx3("strong", { children: f.label }),
1568
+ /* @__PURE__ */ jsxs3("small", { children: [
1569
+ isComputedField(f.name) ? "\u0192 \xB7 " : "",
1570
+ f.type,
1571
+ " \xB7 ",
1572
+ f.group ?? f.name
1573
+ ] })
1574
+ ] })
1575
+ ]
1576
+ },
1577
+ f.name
1578
+ )) }),
1579
+ /* @__PURE__ */ jsx3(
1580
+ "button",
1581
+ {
1582
+ type: "button",
1583
+ className: "qt-link-btn",
1584
+ onClick: () => void api.computed.reload(),
1585
+ children: "Reload shared definitions"
1586
+ }
1587
+ )
1588
+ ] }),
1589
+ /* @__PURE__ */ jsxs3("section", { className: "qt-column-workbench", children: [
1590
+ editing ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
1591
+ /* @__PURE__ */ jsxs3("div", { className: "qt-editor-title", children: [
1592
+ /* @__PURE__ */ jsx3("h3", { children: revision ? "Edit shared computed column" : "New computed column" }),
1593
+ /* @__PURE__ */ jsx3("span", { className: "qt-type-badge", children: validation.plan?.type ?? "Check formula" })
1594
+ ] }),
1595
+ /* @__PURE__ */ jsxs3("label", { className: "qt-editor-label", children: [
1596
+ "Column name",
1597
+ /* @__PURE__ */ jsx3(
1598
+ "input",
1599
+ {
1600
+ className: "qt-input",
1601
+ value: label,
1602
+ maxLength: 200,
1603
+ onChange: (e) => setLabel(e.target.value),
1604
+ placeholder: "e.g. Email domain"
1605
+ }
1606
+ )
1607
+ ] }),
1608
+ /* @__PURE__ */ jsx3(
1609
+ Suspense,
1610
+ {
1611
+ fallback: /* @__PURE__ */ jsx3(
1612
+ "textarea",
1613
+ {
1614
+ "aria-label": "Computed column formula",
1615
+ value: source,
1616
+ onChange: (e) => setSource(e.target.value)
1617
+ }
1618
+ ),
1619
+ children: /* @__PURE__ */ jsx3(
1620
+ FormulaEditor,
1621
+ {
1622
+ value: source,
1623
+ onChange: setSource,
1624
+ fields: api.computed.catalogue.filter(
1625
+ (f) => f.name !== computedFieldName(id)
1626
+ ),
1627
+ compile: (value) => api.computed.compile(value, id)
1628
+ }
1629
+ )
1630
+ }
1631
+ ),
1632
+ /* @__PURE__ */ jsxs3("details", { className: "qt-function-library", children: [
1633
+ /* @__PURE__ */ jsx3("summary", { children: "Functions & examples" }),
1634
+ /* @__PURE__ */ jsx3(
1635
+ "input",
1636
+ {
1637
+ className: "qt-input",
1638
+ "aria-label": "Search functions",
1639
+ placeholder: "Search functions\u2026",
1640
+ value: functionSearch,
1641
+ onChange: (e) => setFunctionSearch(e.target.value)
1642
+ }
1643
+ ),
1644
+ /* @__PURE__ */ jsx3("div", { className: "qt-function-list", children: FORMULA_FUNCTIONS.filter(
1645
+ (f) => `${f.name} ${f.description}`.toLowerCase().includes(functionSearch.toLowerCase())
1646
+ ).map((f) => /* @__PURE__ */ jsxs3(
1647
+ "button",
1648
+ {
1649
+ type: "button",
1650
+ title: f.description,
1651
+ onClick: () => setSource(`${f.name}(${source})`),
1652
+ children: [
1653
+ /* @__PURE__ */ jsx3("code", { children: f.signature }),
1654
+ /* @__PURE__ */ jsx3("small", { children: f.description })
1655
+ ]
1656
+ },
1657
+ f.name
1658
+ )) }),
1659
+ /* @__PURE__ */ jsxs3("p", { children: [
1660
+ "Examples: ",
1661
+ /* @__PURE__ */ jsx3("code", { children: "LEFT([name], 3)" }),
1662
+ " \xB7",
1663
+ " ",
1664
+ /* @__PURE__ */ jsx3("code", { children: 'IF([amount] > 100, "High", "Low")' }),
1665
+ " \xB7",
1666
+ " ",
1667
+ /* @__PURE__ */ jsx3("code", { children: 'REGEX_EXTRACT([email], "@(.+)$", 1)' })
1668
+ ] })
1669
+ ] }),
1670
+ /* @__PURE__ */ jsxs3("div", { className: "qt-definition-actions", children: [
1671
+ /* @__PURE__ */ jsx3(
1672
+ "button",
1673
+ {
1674
+ type: "button",
1675
+ className: "qt-btn",
1676
+ disabled: saving || !!validation.error || !label.trim(),
1677
+ onClick: () => void save(),
1678
+ children: saving ? "Saving\u2026" : revision ? "Save shared definition" : "Save definition & add column"
1679
+ }
1680
+ ),
1681
+ revision && /* @__PURE__ */ jsx3(
1682
+ "button",
1683
+ {
1684
+ type: "button",
1685
+ className: "qt-link-btn",
1686
+ onClick: () => edit(id),
1687
+ children: "Reload editor from catalogue"
1688
+ }
1689
+ )
1690
+ ] }),
1691
+ /* @__PURE__ */ jsx3("p", { className: "qt-muted", children: "Saving changes this definition across all queries. Cancel below only discards column layout changes." })
1692
+ ] }) : /* @__PURE__ */ jsxs3("div", { className: "qt-editor-title", children: [
1693
+ /* @__PURE__ */ jsxs3("div", { children: [
1694
+ /* @__PURE__ */ jsx3("h3", { children: chosen?.label ?? "Explore a column" }),
1695
+ /* @__PURE__ */ jsxs3("p", { className: "qt-muted", children: [
1696
+ chosen?.name,
1697
+ " ",
1698
+ chosen ? `\xB7 ${chosen.type}` : ""
1699
+ ] })
1700
+ ] }),
1701
+ chosen && isComputedField(chosen.name) && /* @__PURE__ */ jsxs3(Fragment3, { children: [
1702
+ /* @__PURE__ */ jsx3(
1703
+ "button",
1704
+ {
1705
+ type: "button",
1706
+ className: "qt-btn",
1707
+ onClick: () => edit(chosen.name.slice(10)),
1708
+ children: "Edit definition"
1709
+ }
1710
+ ),
1711
+ /* @__PURE__ */ jsx3(
1712
+ "button",
1713
+ {
1714
+ type: "button",
1715
+ className: "qt-btn",
1716
+ onClick: () => {
1717
+ const def = api.computed.definitions.find(
1718
+ (d) => computedFieldName(d.id) === chosen.name
1719
+ );
1720
+ edit();
1721
+ if (def) {
1722
+ setLabel(`${def.label} copy`);
1723
+ setSource(def.expression.source);
1724
+ }
1725
+ },
1726
+ children: "Duplicate"
1727
+ }
1728
+ )
1729
+ ] })
1730
+ ] }),
1731
+ /* @__PURE__ */ jsxs3("div", { className: "qt-preview-controls", children: [
1732
+ /* @__PURE__ */ jsxs3("label", { children: [
1733
+ "Rows to process",
1734
+ " ",
1735
+ /* @__PURE__ */ jsx3(
1736
+ "input",
1737
+ {
1738
+ className: "qt-input",
1739
+ type: "number",
1740
+ min: 1,
1741
+ max: 1e4,
1742
+ step: 1,
1743
+ value: Number.isNaN(count) ? "" : count,
1744
+ onChange: (e) => setCount(e.target.valueAsNumber)
1745
+ }
1746
+ )
1747
+ ] }),
1748
+ /* @__PURE__ */ jsx3(
1749
+ "button",
1750
+ {
1751
+ type: "button",
1752
+ className: "qt-btn",
1753
+ onClick: () => setRetry((n) => n + 1),
1754
+ children: "Refresh preview"
1755
+ }
1756
+ ),
1757
+ /* @__PURE__ */ jsxs3("label", { children: [
1758
+ "Show",
1759
+ " ",
1760
+ /* @__PURE__ */ jsxs3(
1761
+ "select",
1762
+ {
1763
+ value: filter,
1764
+ onChange: (e) => {
1765
+ setFilter(e.target.value);
1766
+ setPage(0);
1767
+ },
1768
+ children: [
1769
+ /* @__PURE__ */ jsx3("option", { value: "all", children: "All results" }),
1770
+ /* @__PURE__ */ jsx3("option", { value: "nulls", children: "Null results" }),
1771
+ /* @__PURE__ */ jsx3("option", { value: "errors", children: "Errors" })
1772
+ ]
1773
+ }
1774
+ )
1775
+ ] })
1776
+ ] }),
1777
+ /* @__PURE__ */ jsx3("p", { className: "qt-muted", children: "First matching rows in current sort order, starting at row 1. Counts describe the processed sample. Maximum 10,000 rows." }),
1778
+ (!Number.isInteger(count) || count < 1 || count > 1e4) && /* @__PURE__ */ jsx3("p", { role: "alert", children: "Choose a whole number between 1 and 10,000." }),
1779
+ validation.error && /* @__PURE__ */ jsx3("p", { className: "qt-formula-error", role: "alert", children: validation.error }),
1780
+ error && /* @__PURE__ */ jsx3("p", { className: "qt-formula-error", role: "alert", children: error }),
1781
+ notice && /* @__PURE__ */ jsx3("p", { className: "qt-editor-notice", role: "status", children: notice }),
1782
+ loading && /* @__PURE__ */ jsx3("p", { role: "status", children: "Processing preview\u2026" }),
1783
+ preview && /* @__PURE__ */ jsxs3(Fragment3, { children: [
1784
+ /* @__PURE__ */ jsxs3("p", { className: "qt-preview-summary", role: "status", children: [
1785
+ /* @__PURE__ */ jsx3("strong", { children: preview.processed.toLocaleString() }),
1786
+ " of",
1787
+ " ",
1788
+ preview.total.toLocaleString(),
1789
+ " matching rows \xB7",
1790
+ " ",
1791
+ /* @__PURE__ */ jsx3("strong", { children: preview.groups.length.toLocaleString() }),
1792
+ " ",
1793
+ "distinct combinations \xB7 ",
1794
+ preview.nulls.toLocaleString(),
1795
+ " null \xB7 ",
1796
+ preview.errors.toLocaleString(),
1797
+ " errors",
1798
+ preview.processed === preview.total ? " \xB7 complete filtered set" : " \xB7 sample"
1799
+ ] }),
1800
+ /* @__PURE__ */ jsx3("div", { className: "qt-preview-table-wrap", children: /* @__PURE__ */ jsxs3("table", { className: "qt-preview-table", children: [
1801
+ /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsxs3("tr", { children: [
1802
+ editing && preview.dependencies.map((name, index) => /* @__PURE__ */ jsx3(
1803
+ "th",
1804
+ {
1805
+ "aria-sort": previewAriaSort({
1806
+ kind: "input",
1807
+ index
1808
+ }),
1809
+ children: previewSortHeader(
1810
+ api.computed.catalogue.find(
1811
+ (f) => f.name === name
1812
+ )?.label ?? name,
1813
+ { kind: "input", index }
1814
+ )
1815
+ },
1816
+ name
1817
+ )),
1818
+ /* @__PURE__ */ jsx3("th", { "aria-sort": previewAriaSort({ kind: "result" }), children: previewSortHeader(editing ? "Result" : "Value", {
1819
+ kind: "result"
1820
+ }) }),
1821
+ /* @__PURE__ */ jsx3("th", { "aria-sort": previewAriaSort({ kind: "count" }), children: previewSortHeader("Count", { kind: "count" }, "desc") }),
1822
+ /* @__PURE__ */ jsx3("th", { "aria-sort": previewAriaSort({ kind: "percentage" }), children: previewSortHeader("%", { kind: "percentage" }, "desc") })
1823
+ ] }) }),
1824
+ /* @__PURE__ */ jsx3("tbody", { children: shown.map((group, i) => /* @__PURE__ */ jsxs3("tr", { children: [
1825
+ editing && group.inputs.map((value, j) => /* @__PURE__ */ jsx3("td", { title: display(value), children: display(value) }, j)),
1826
+ /* @__PURE__ */ jsxs3(
1827
+ "td",
1828
+ {
1829
+ className: group.result.error ? "qt-formula-error" : "",
1830
+ title: group.result.error ?? display(group.result.value),
1831
+ children: [
1832
+ group.result.error ?? display(group.result.value),
1833
+ group.result.regex && /* @__PURE__ */ jsx3(
1834
+ "button",
1835
+ {
1836
+ type: "button",
1837
+ className: "qt-link-btn",
1838
+ onClick: () => setInspection(group.result.regex),
1839
+ children: "Inspect match"
1840
+ }
1841
+ )
1842
+ ]
1843
+ }
1844
+ ),
1845
+ /* @__PURE__ */ jsx3("td", { children: group.count.toLocaleString() }),
1846
+ /* @__PURE__ */ jsxs3("td", { children: [
1847
+ (100 * group.count / preview.processed).toFixed(
1848
+ 1
1849
+ ),
1850
+ "%"
1851
+ ] })
1852
+ ] }, i)) })
1853
+ ] }) }),
1854
+ inspection && /* @__PURE__ */ jsxs3("div", { className: "qt-regex-inspector", children: [
1855
+ /* @__PURE__ */ jsx3("h4", { children: "Regex match" }),
1856
+ /* @__PURE__ */ jsxs3("pre", { children: [
1857
+ inspection.input.slice(0, inspection.start),
1858
+ /* @__PURE__ */ jsx3("mark", { children: inspection.input.slice(
1859
+ inspection.start,
1860
+ inspection.end
1861
+ ) }),
1862
+ inspection.input.slice(inspection.end)
1863
+ ] }),
1864
+ /* @__PURE__ */ jsx3("dl", { children: inspection.groups.map((value, i) => /* @__PURE__ */ jsxs3("div", { children: [
1865
+ /* @__PURE__ */ jsxs3("dt", { children: [
1866
+ "Group ",
1867
+ i,
1868
+ i === 0 ? " (whole match)" : ""
1869
+ ] }),
1870
+ /* @__PURE__ */ jsx3("dd", { children: display(value) })
1871
+ ] }, i)) })
1872
+ ] }),
1873
+ !groups.length && /* @__PURE__ */ jsx3("p", { children: "No matching results in this sample." }),
1874
+ groups.length > 50 && /* @__PURE__ */ jsxs3("div", { className: "qt-preview-pages", children: [
1875
+ /* @__PURE__ */ jsx3(
1876
+ "button",
1877
+ {
1878
+ type: "button",
1879
+ className: "qt-btn",
1880
+ disabled: page === 0,
1881
+ onClick: () => setPage((n) => n - 1),
1882
+ children: "Previous"
1883
+ }
1884
+ ),
1885
+ /* @__PURE__ */ jsxs3("span", { children: [
1886
+ "Combinations ",
1887
+ page * 50 + 1,
1888
+ "\u2013",
1889
+ Math.min((page + 1) * 50, groups.length),
1890
+ " of",
1891
+ " ",
1892
+ groups.length
1893
+ ] }),
1894
+ /* @__PURE__ */ jsx3(
1895
+ "button",
1896
+ {
1897
+ type: "button",
1898
+ className: "qt-btn",
1899
+ disabled: (page + 1) * 50 >= groups.length,
1900
+ onClick: () => setPage((n) => n + 1),
1901
+ children: "Next"
1902
+ }
1903
+ )
1904
+ ] })
1905
+ ] })
1906
+ ] })
1907
+ ] }),
1908
+ /* @__PURE__ */ jsxs3("footer", { className: "qt-select-editor-footer", children: [
1909
+ /* @__PURE__ */ jsx3("span", { className: "qt-muted", children: "Computed columns are display-only. Select at least one column." }),
1910
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "qt-btn", onClick: onClose, children: "Cancel layout changes" }),
1911
+ /* @__PURE__ */ jsx3(
1912
+ "button",
1913
+ {
1914
+ type: "button",
1915
+ className: "qt-btn qt-primary",
1916
+ disabled: !columns.length || columns.length > 200,
1917
+ onClick: () => {
1918
+ api.setQuery((q) => ({ ...q, select: columns }));
1919
+ onClose();
1920
+ },
1921
+ children: "Apply columns"
1922
+ }
1923
+ )
1924
+ ] })
1925
+ ]
1926
+ }
1927
+ ) });
1928
+ }
1929
+
976
1930
  // src/QueryBuilder.tsx
977
- import { useEffect as useEffect4, useId, useMemo as useMemo3, useRef as useRef2, useState as useState4 } from "react";
1931
+ import { Fragment as Fragment5, useEffect as useEffect5, useId as useId2, useMemo as useMemo4, useRef as useRef3, useState as useState5 } from "react";
978
1932
  import {
979
1933
  NULLARY_OPS as NULLARY_OPS2,
980
1934
  aggOpNeedsField,
981
1935
  aggOpsForField,
1936
+ encodeQuery,
982
1937
  filterValues as filterValuesFor,
983
1938
  isGroupable,
984
1939
  isMeasurable,
1940
+ isOrGroup,
1941
+ opPairsForField as opPairsForField2,
1942
+ predicatesOf,
985
1943
  queriesEqual,
986
- isFilterable,
987
- isSelectable,
1944
+ isFilterable as isFilterable2,
1945
+ isSelectable as isSelectable2,
988
1946
  isSortable as isSortable2,
989
1947
  opsForField as opsForField2
990
1948
  } from "@pythia-software/query-table-core";
991
1949
 
992
1950
  // src/FieldPicker.tsx
993
- import { useMemo as useMemo2, useState as useState3 } from "react";
994
- import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1951
+ import { useMemo as useMemo3, useState as useState4 } from "react";
1952
+ import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
995
1953
  var UNGROUPED = "other";
996
1954
  function FieldPicker({
997
1955
  fields,
@@ -1001,10 +1959,10 @@ function FieldPicker({
1001
1959
  onPick,
1002
1960
  onClose
1003
1961
  }) {
1004
- const [search, setSearch] = useState3("");
1962
+ const [search, setSearch] = useState4("");
1005
1963
  const needle = search.trim().toLowerCase();
1006
- const excludedSet = useMemo2(() => new Set(excluded ?? []), [excluded]);
1007
- const matched = useMemo2(
1964
+ const excludedSet = useMemo3(() => new Set(excluded ?? []), [excluded]);
1965
+ const matched = useMemo3(
1008
1966
  () => fields.filter((f) => {
1009
1967
  if (excludedSet.has(f.name)) return false;
1010
1968
  if (!needle) return true;
@@ -1012,10 +1970,10 @@ function FieldPicker({
1012
1970
  }),
1013
1971
  [fields, excludedSet, needle]
1014
1972
  );
1015
- const grouped = useMemo2(() => groupByGroup(matched), [matched]);
1973
+ const grouped = useMemo3(() => groupByGroup(matched), [matched]);
1016
1974
  const firstEnabled = matched.find((f) => isEnabled(f, stats, gateOnDistinct));
1017
- return /* @__PURE__ */ jsxs3("div", { className: "qt-picker", children: [
1018
- /* @__PURE__ */ jsx3(
1975
+ return /* @__PURE__ */ jsxs4("div", { className: "qt-picker", children: [
1976
+ /* @__PURE__ */ jsx4(
1019
1977
  "input",
1020
1978
  {
1021
1979
  autoFocus: true,
@@ -1034,40 +1992,40 @@ function FieldPicker({
1034
1992
  onBlur: () => setTimeout(() => onClose?.(), 150)
1035
1993
  }
1036
1994
  ),
1037
- /* @__PURE__ */ jsx3("div", { className: "qt-picker-list", children: matched.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "qt-picker-empty", children: "no matches" }) : grouped.map(([group, fs]) => /* @__PURE__ */ jsxs3("div", { className: "qt-picker-group", children: [
1038
- /* @__PURE__ */ jsx3("div", { className: "qt-picker-group-label", children: group }),
1995
+ /* @__PURE__ */ jsx4("div", { className: "qt-picker-list", children: matched.length === 0 ? /* @__PURE__ */ jsx4("div", { className: "qt-picker-empty", children: "no matches" }) : grouped.map(([group, fs]) => /* @__PURE__ */ jsxs4("div", { className: "qt-picker-group", children: [
1996
+ /* @__PURE__ */ jsx4("div", { className: "qt-picker-group-label", children: group }),
1039
1997
  fs.map((f) => {
1040
1998
  const enabled = isEnabled(f, stats, gateOnDistinct);
1041
1999
  const s = stats?.[f.name];
1042
2000
  const aliasHit = aliasMatch(f, needle);
1043
- return /* @__PURE__ */ jsxs3(
2001
+ return /* @__PURE__ */ jsxs4(
1044
2002
  "button",
1045
2003
  {
1046
2004
  type: "button",
1047
2005
  disabled: !enabled,
1048
2006
  className: enabled ? "qt-picker-item" : "qt-picker-item qt-picker-item--disabled",
1049
2007
  title: enabled ? f.name : "not useful as a filter (\u22641 distinct value)",
1050
- onMouseDown: (e) => {
2008
+ onPointerDown: (e) => {
1051
2009
  if (!enabled) return;
1052
2010
  e.preventDefault();
1053
2011
  onPick(f);
1054
2012
  setSearch("");
1055
2013
  },
1056
2014
  children: [
1057
- /* @__PURE__ */ jsxs3("span", { className: "qt-picker-row", children: [
1058
- /* @__PURE__ */ jsx3("span", { className: "qt-picker-label", children: f.label }),
1059
- /* @__PURE__ */ jsx3("span", { className: "qt-picker-name", children: f.name })
2015
+ /* @__PURE__ */ jsxs4("span", { className: "qt-picker-row", children: [
2016
+ /* @__PURE__ */ jsx4("span", { className: "qt-picker-label", children: f.label }),
2017
+ /* @__PURE__ */ jsx4("span", { className: "qt-picker-name", children: f.name })
1060
2018
  ] }),
1061
- aliasHit && /* @__PURE__ */ jsxs3("span", { className: "qt-picker-alias", children: [
2019
+ aliasHit && /* @__PURE__ */ jsxs4("span", { className: "qt-picker-alias", children: [
1062
2020
  "matches: ",
1063
2021
  aliasHit
1064
2022
  ] }),
1065
- s && /* @__PURE__ */ jsxs3("span", { className: "qt-picker-stats", children: [
1066
- s.distinct != null && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2023
+ s && /* @__PURE__ */ jsxs4("span", { className: "qt-picker-stats", children: [
2024
+ s.distinct != null && /* @__PURE__ */ jsxs4(Fragment4, { children: [
1067
2025
  s.distinct,
1068
2026
  " distinct"
1069
2027
  ] }),
1070
- s.min != null && s.max != null && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2028
+ s.min != null && s.max != null && /* @__PURE__ */ jsxs4(Fragment4, { children: [
1071
2029
  " ",
1072
2030
  "\xB7 ",
1073
2031
  fmtStat(s.min),
@@ -1121,86 +2079,97 @@ function fmtStat(v) {
1121
2079
  }
1122
2080
 
1123
2081
  // src/SavedQueriesModal.tsx
1124
- import { useEffect as useEffect3 } from "react";
1125
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2082
+ import { useEffect as useEffect4 } from "react";
2083
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1126
2084
  function SavedQueriesModal({ saved, onClose, onLoad }) {
1127
- useEffect3(() => {
2085
+ useEffect4(() => {
1128
2086
  function onKey(e) {
1129
2087
  if (e.key === "Escape") onClose();
1130
2088
  }
1131
- window.addEventListener("keydown", onKey);
1132
- return () => window.removeEventListener("keydown", onKey);
1133
- }, [onClose]);
1134
- return /* @__PURE__ */ jsx4("div", { className: "qt-modal-backdrop", onClick: onClose, children: /* @__PURE__ */ jsxs4("div", { className: "qt-modal", onClick: (e) => e.stopPropagation(), role: "dialog", "aria-label": "Saved queries", children: [
1135
- /* @__PURE__ */ jsx4("h3", { className: "qt-modal-title", children: "Saved queries" }),
1136
- saved.loading ? /* @__PURE__ */ jsx4("p", { className: "qt-muted", children: "Loading\u2026" }) : saved.items.length === 0 ? /* @__PURE__ */ jsx4("p", { className: "qt-muted", children: "None saved yet. Save the current query from the toolbar." }) : /* @__PURE__ */ jsx4("ul", { className: "qt-saved-list", children: saved.items.map((s) => {
1137
- const filters = s.query.where.length;
1138
- const isDefault = saved.defaultId === s.id;
1139
- return /* @__PURE__ */ jsxs4("li", { className: "qt-saved-item", children: [
1140
- /* @__PURE__ */ jsxs4(
1141
- "button",
1142
- {
1143
- type: "button",
1144
- className: "qt-saved-pick",
1145
- onClick: () => {
1146
- onLoad?.(s.id);
1147
- saved.load(s.id);
1148
- onClose();
1149
- },
1150
- children: [
1151
- /* @__PURE__ */ jsx4("span", { className: "qt-saved-name", children: s.name }),
1152
- /* @__PURE__ */ jsxs4("span", { className: "qt-saved-meta qt-muted", children: [
1153
- isDefault ? "Default view \xB7 " : "",
1154
- new Date(s.savedAt).toLocaleString(),
1155
- " \xB7 ",
1156
- filters,
1157
- " filter",
1158
- filters === 1 ? "" : "s"
1159
- ] })
1160
- ]
1161
- }
1162
- ),
1163
- isDefault ? /* @__PURE__ */ jsx4(
1164
- "button",
1165
- {
1166
- type: "button",
1167
- className: "qt-btn qt-saved-default",
1168
- title: "Clear default view",
1169
- onClick: () => void saved.clearDefault(),
1170
- children: "default"
1171
- }
1172
- ) : /* @__PURE__ */ jsx4(
1173
- "button",
1174
- {
1175
- type: "button",
1176
- className: "qt-btn qt-saved-default",
1177
- title: "Use as default view",
1178
- onClick: () => void saved.setDefault(s.id),
1179
- children: "make default"
1180
- }
1181
- ),
1182
- /* @__PURE__ */ jsx4(
1183
- "button",
1184
- {
1185
- type: "button",
1186
- className: "qt-btn qt-saved-delete",
1187
- title: "Delete saved query",
1188
- onClick: () => void saved.remove(s.id),
1189
- children: "delete"
1190
- }
1191
- )
1192
- ] }, s.id);
1193
- }) }),
1194
- /* @__PURE__ */ jsx4("div", { className: "qt-modal-footer", children: /* @__PURE__ */ jsx4("button", { type: "button", className: "qt-btn", onClick: onClose, children: "close" }) })
1195
- ] }) });
2089
+ window.addEventListener("keydown", onKey);
2090
+ return () => window.removeEventListener("keydown", onKey);
2091
+ }, [onClose]);
2092
+ return /* @__PURE__ */ jsx5("div", { className: "qt-modal-backdrop", onClick: onClose, children: /* @__PURE__ */ jsxs5(
2093
+ "div",
2094
+ {
2095
+ className: "qt-modal",
2096
+ onClick: (e) => e.stopPropagation(),
2097
+ role: "dialog",
2098
+ "aria-modal": "true",
2099
+ "aria-label": "Saved queries",
2100
+ children: [
2101
+ /* @__PURE__ */ jsx5("h3", { className: "qt-modal-title", children: "Saved queries" }),
2102
+ saved.loading ? /* @__PURE__ */ jsx5("p", { className: "qt-muted", children: "Loading\u2026" }) : saved.items.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "qt-muted", children: "None saved yet. Save the current query from the toolbar." }) : /* @__PURE__ */ jsx5("ul", { className: "qt-saved-list", children: saved.items.map((s) => {
2103
+ const filters = s.query.where.length;
2104
+ const isDefault = saved.defaultId === s.id;
2105
+ return /* @__PURE__ */ jsxs5("li", { className: "qt-saved-item", children: [
2106
+ /* @__PURE__ */ jsxs5(
2107
+ "button",
2108
+ {
2109
+ type: "button",
2110
+ className: "qt-saved-pick",
2111
+ onClick: () => {
2112
+ onLoad?.(s.id);
2113
+ saved.load(s.id);
2114
+ onClose();
2115
+ },
2116
+ children: [
2117
+ /* @__PURE__ */ jsx5("span", { className: "qt-saved-name", children: s.name }),
2118
+ /* @__PURE__ */ jsxs5("span", { className: "qt-saved-meta qt-muted", children: [
2119
+ isDefault ? "Default view \xB7 " : "",
2120
+ new Date(s.savedAt).toLocaleString(),
2121
+ " \xB7 ",
2122
+ filters,
2123
+ " filter",
2124
+ filters === 1 ? "" : "s"
2125
+ ] })
2126
+ ]
2127
+ }
2128
+ ),
2129
+ isDefault ? /* @__PURE__ */ jsx5(
2130
+ "button",
2131
+ {
2132
+ type: "button",
2133
+ className: "qt-btn qt-saved-default",
2134
+ title: "Clear default view",
2135
+ onClick: () => void saved.clearDefault(),
2136
+ children: "default"
2137
+ }
2138
+ ) : /* @__PURE__ */ jsx5(
2139
+ "button",
2140
+ {
2141
+ type: "button",
2142
+ className: "qt-btn qt-saved-default",
2143
+ title: "Use as default view",
2144
+ onClick: () => void saved.setDefault(s.id),
2145
+ children: "make default"
2146
+ }
2147
+ ),
2148
+ /* @__PURE__ */ jsx5(
2149
+ "button",
2150
+ {
2151
+ type: "button",
2152
+ className: "qt-btn qt-saved-delete",
2153
+ title: "Delete saved query",
2154
+ onClick: () => void saved.remove(s.id),
2155
+ children: "delete"
2156
+ }
2157
+ )
2158
+ ] }, s.id);
2159
+ }) }),
2160
+ /* @__PURE__ */ jsx5("div", { className: "qt-modal-footer", children: /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-btn", onClick: onClose, children: "close" }) })
2161
+ ]
2162
+ }
2163
+ ) });
1196
2164
  }
1197
2165
 
1198
2166
  // src/QueryBuilder.tsx
1199
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2167
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1200
2168
  var cx3 = (...parts) => parts.filter((p) => Boolean(p)).join(" ");
1201
2169
  var MAX_AUTO_REFRESH_POLLS = 1e3;
1202
2170
  var DEFAULT_AUTO_REFRESH_FREQUENCY_MS = 2e4;
1203
2171
  var DEFAULT_AUTO_REFRESH_TURN_OFF_AFTER_MS = 5 * 6e4;
2172
+ var SHARE_FEEDBACK_MS = 2e3;
1204
2173
  var AUTO_REFRESH_FREQUENCIES = [
1205
2174
  { label: "Every 5s", value: 5e3 },
1206
2175
  { label: "Every 10s", value: 1e4 },
@@ -1239,8 +2208,8 @@ function formatStatusTime(ms) {
1239
2208
  return new Date(ms).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", second: "2-digit" });
1240
2209
  }
1241
2210
  function useFieldHasNull(api, fieldName) {
1242
- const [hasNull, setHasNull] = useState4(void 0);
1243
- useEffect4(() => {
2211
+ const [hasNull, setHasNull] = useState5(void 0);
2212
+ useEffect5(() => {
1244
2213
  if (!fieldName) return;
1245
2214
  let cancelled = false;
1246
2215
  void api.filterValues(fieldName, "").then((r) => {
@@ -1256,17 +2225,27 @@ function useFieldHasNull(api, fieldName) {
1256
2225
  }, [api, fieldName]);
1257
2226
  return hasNull;
1258
2227
  }
1259
- function whereClauseAsText(clause, byName) {
2228
+ var CONVERGED_NEGATIVE_OPS = /* @__PURE__ */ new Set(["!=", "is_not_null", "not_matches_regex"]);
2229
+ function positiveOpsForField(field) {
2230
+ return opsForField2(field).filter((op) => !CONVERGED_NEGATIVE_OPS.has(op));
2231
+ }
2232
+ function predicateAsText(clause, byName) {
1260
2233
  const field = byName.get(clause.field)?.label ?? clause.field;
1261
2234
  const op = clause.op.replace(/_/g, " ");
1262
- if (NULLARY_OPS2.has(clause.op)) return `${field} ${op}`;
2235
+ const not = clause.negated ? "not " : "";
2236
+ if (NULLARY_OPS2.has(clause.op)) return `${field} ${not}${op}`;
1263
2237
  const value = clause.value === "" ? "''" : clause.value;
1264
- return `${field} ${op} ${value}`;
2238
+ return `${field} ${not}${op} ${value}`;
2239
+ }
2240
+ function whereTermAsText(term, byName) {
2241
+ if (isOrGroup(term)) return `(${term.any.map((c) => predicateAsText(c, byName)).join(" or ")})`;
2242
+ return predicateAsText(term, byName);
1265
2243
  }
1266
2244
  function orderByAsText(term, byName) {
1267
2245
  const field = byName.get(term.field)?.label ?? term.field;
2246
+ const expression = term.extract ? `regex_extract(${field}, /${term.extract.regex}/)` : field;
1268
2247
  const nulls = term.nulls ? ` nulls ${term.nulls}` : "";
1269
- return `${field} ${term.dir}${nulls}`;
2248
+ return `${expression} ${term.dir}${nulls}`;
1270
2249
  }
1271
2250
  function chipFieldWidthChars(text, fallback = 5, max = 30) {
1272
2251
  return Math.min(max, Math.max(fallback, (text || "").length + 1));
@@ -1279,12 +2258,12 @@ function chipFieldWidthForSelect(text, fallback = 3, max = 30) {
1279
2258
  return chipFieldWidthChars(text, fallback, max) + 2;
1280
2259
  }
1281
2260
  function buildCollapsedSummary(where, orderBy, byName) {
1282
- const whereText = where.length === 0 ? "all rows" : where.map((c) => whereClauseAsText(c, byName)).join(" and ");
2261
+ const whereText = whereClauseSummaryText(where, byName);
1283
2262
  const orderText = orderBy.length === 0 ? "(default)" : orderBy.map((term) => orderByAsText(term, byName)).join(", ");
1284
2263
  return `WHERE ${whereText} ORDER BY ${orderText}`;
1285
2264
  }
1286
2265
  function whereClauseSummaryText(where, byName) {
1287
- return where.length === 0 ? "all rows" : where.map((c) => whereClauseAsText(c, byName)).join(" and ");
2266
+ return where.length === 0 ? "all rows" : where.map((t) => whereTermAsText(t, byName)).join(" and ");
1288
2267
  }
1289
2268
  function orderBySummaryText(orderBy, byName) {
1290
2269
  return orderBy.length === 0 ? "(default)" : orderBy.map((term) => orderByAsText(term, byName)).join(", ");
@@ -1300,14 +2279,15 @@ function QueryBuilder({
1300
2279
  defaultCollapsed = false
1301
2280
  }) {
1302
2281
  const UNSAVED_QUERY_EDIT_ID = "__qt-unsaved-query__";
1303
- const [showSaved, setShowSaved] = useState4(false);
1304
- const [showAutoRefresh, setShowAutoRefresh] = useState4(false);
1305
- const [autoRefreshFrequencyMs, setAutoRefreshFrequencyMs] = useState4(DEFAULT_AUTO_REFRESH_FREQUENCY_MS);
1306
- const [autoRefreshTurnOffAfterMs, setAutoRefreshTurnOffAfterMs] = useState4(DEFAULT_AUTO_REFRESH_TURN_OFF_AFTER_MS);
1307
- const [editingSavedId, setEditingSavedId] = useState4(null);
1308
- const [editingSavedName, setEditingSavedName] = useState4("");
1309
- const [lastSavedId, setLastSavedId] = useState4(null);
1310
- const [collapsedFallback, setCollapsedFallback] = useState4(defaultCollapsed);
2282
+ const [showSaved, setShowSaved] = useState5(false);
2283
+ const [shareCopied, setShareCopied] = useState5(false);
2284
+ const [showAutoRefresh, setShowAutoRefresh] = useState5(false);
2285
+ const [autoRefreshFrequencyMs, setAutoRefreshFrequencyMs] = useState5(DEFAULT_AUTO_REFRESH_FREQUENCY_MS);
2286
+ const [autoRefreshTurnOffAfterMs, setAutoRefreshTurnOffAfterMs] = useState5(DEFAULT_AUTO_REFRESH_TURN_OFF_AFTER_MS);
2287
+ const [editingSavedId, setEditingSavedId] = useState5(null);
2288
+ const [editingSavedName, setEditingSavedName] = useState5("");
2289
+ const [lastSavedId, setLastSavedId] = useState5(null);
2290
+ const [collapsedFallback, setCollapsedFallback] = useState5(defaultCollapsed);
1311
2291
  const collapsedControlled = typeof collapsed !== "undefined";
1312
2292
  const isCollapsed = collapsedControlled ? collapsed : collapsedFallback;
1313
2293
  const setCollapsed = (next) => {
@@ -1317,31 +2297,31 @@ function QueryBuilder({
1317
2297
  }
1318
2298
  setCollapsedFallback(next);
1319
2299
  };
1320
- const [lastUpdatedAt, setLastUpdatedAt] = useState4(null);
1321
- const [lastFailureAt, setLastFailureAt] = useState4(null);
1322
- const byName = useMemo3(() => new Map(fields.map((f) => [f.name, f])), [fields]);
1323
- const activeSavedQuery = useMemo3(
2300
+ const [lastUpdatedAt, setLastUpdatedAt] = useState5(null);
2301
+ const [lastFailureAt, setLastFailureAt] = useState5(null);
2302
+ const byName = useMemo4(() => new Map(fields.map((f) => [f.name, f])), [fields]);
2303
+ const activeSavedQuery = useMemo4(
1324
2304
  () => api.saved.items.find((item) => queriesEqual(item.query, api.query)),
1325
2305
  [api.query, api.saved.items]
1326
2306
  );
1327
- const lastSavedQuery = useMemo3(
2307
+ const lastSavedQuery = useMemo4(
1328
2308
  () => api.saved.items.find((item) => item.id === lastSavedId) ?? null,
1329
2309
  [api.saved.items, lastSavedId]
1330
2310
  );
1331
2311
  const isSavedQuery = activeSavedQuery != null;
1332
2312
  const isEditingUnsaved = editingSavedId === UNSAVED_QUERY_EDIT_ID;
1333
- const bodyId = useId();
2313
+ const bodyId = useId2();
1334
2314
  const isEditingSaved = editingSavedId != null && activeSavedQuery?.id === editingSavedId;
1335
2315
  const showUpdateButton = Boolean(lastSavedQuery) && !activeSavedQuery;
1336
- const collapsedSummary = useMemo3(
2316
+ const collapsedSummary = useMemo4(
1337
2317
  () => buildCollapsedSummary(api.query.where, api.query.orderBy, byName),
1338
2318
  [api.query.where, api.query.orderBy, byName]
1339
2319
  );
1340
- const collapsedWhereText = useMemo3(
2320
+ const collapsedWhereText = useMemo4(
1341
2321
  () => whereClauseSummaryText(api.query.where, byName),
1342
2322
  [api.query.where, byName]
1343
2323
  );
1344
- const collapsedOrderText = useMemo3(
2324
+ const collapsedOrderText = useMemo4(
1345
2325
  () => orderBySummaryText(api.query.orderBy, byName),
1346
2326
  [api.query.orderBy, byName]
1347
2327
  );
@@ -1353,13 +2333,27 @@ function QueryBuilder({
1353
2333
  const lastUpdatedText = formatStatusTime(lastUpdatedAt);
1354
2334
  const failureText = api.error ? `Error: ${api.error.message}` : lastFailureAt ? `Last failure at ${formatStatusTime(lastFailureAt)}` : null;
1355
2335
  const isBusy = Boolean(running) || api.loading;
1356
- const lastRowsRef = useRef2(null);
1357
- const lastTotalRef = useRef2(null);
1358
- const previousLoadingRef = useRef2(api.loading);
1359
- const autoRefreshRef = useRef2(null);
1360
- const autoRefreshPopoverRef = useRef2(null);
1361
- const [autoRefreshPopoverAlignRight, setAutoRefreshPopoverAlignRight] = useState4(false);
1362
- useEffect4(() => {
2336
+ const lastRowsRef = useRef3(null);
2337
+ const lastTotalRef = useRef3(null);
2338
+ const previousLoadingRef = useRef3(api.loading);
2339
+ const autoRefreshRef = useRef3(null);
2340
+ const autoRefreshPopoverRef = useRef3(null);
2341
+ const [autoRefreshPopoverAlignRight, setAutoRefreshPopoverAlignRight] = useState5(false);
2342
+ const shareFeedbackTimerRef = useRef3(null);
2343
+ useEffect5(
2344
+ () => () => {
2345
+ if (shareFeedbackTimerRef.current) clearTimeout(shareFeedbackTimerRef.current);
2346
+ },
2347
+ []
2348
+ );
2349
+ useEffect5(() => {
2350
+ setShareCopied(false);
2351
+ if (shareFeedbackTimerRef.current) {
2352
+ clearTimeout(shareFeedbackTimerRef.current);
2353
+ shareFeedbackTimerRef.current = null;
2354
+ }
2355
+ }, [api.query]);
2356
+ useEffect5(() => {
1363
2357
  if (activeSavedQuery) {
1364
2358
  setLastSavedId(activeSavedQuery.id);
1365
2359
  if (!isEditingSaved) {
@@ -1373,11 +2367,11 @@ function QueryBuilder({
1373
2367
  setEditingSavedName("");
1374
2368
  }
1375
2369
  }, [activeSavedQuery, isEditingSaved, isEditingUnsaved]);
1376
- useEffect4(() => {
2370
+ useEffect5(() => {
1377
2371
  if (!lastSavedId || api.saved.items.some((item) => item.id === lastSavedId)) return;
1378
2372
  setLastSavedId(null);
1379
2373
  }, [api.saved.items, lastSavedId]);
1380
- useEffect4(() => {
2374
+ useEffect5(() => {
1381
2375
  const hasData = api.rows.length > 0 || total != null;
1382
2376
  const rowsChanged = lastRowsRef.current !== api.rows;
1383
2377
  const totalChanged = lastTotalRef.current !== total;
@@ -1472,15 +2466,15 @@ function QueryBuilder({
1472
2466
  return;
1473
2467
  }
1474
2468
  }
1475
- useEffect4(() => {
2469
+ useEffect5(() => {
1476
2470
  if (isCollapsed) setShowSaved(false);
1477
2471
  }, [isCollapsed]);
1478
- useEffect4(() => {
2472
+ useEffect5(() => {
1479
2473
  if (!showAutoRefresh || !autoRefreshStatus) return;
1480
2474
  setAutoRefreshFrequencyMs(autoRefreshStatus.frequencyMs);
1481
2475
  setAutoRefreshTurnOffAfterMs(autoRefreshStatus.turnOffAfterMs);
1482
2476
  }, [showAutoRefresh, autoRefreshStatus]);
1483
- useEffect4(() => {
2477
+ useEffect5(() => {
1484
2478
  if (!showAutoRefresh) return;
1485
2479
  function onDocumentClick(event) {
1486
2480
  const wrapper = autoRefreshRef.current;
@@ -1523,11 +2517,27 @@ function QueryBuilder({
1523
2517
  saveFailed(error);
1524
2518
  }
1525
2519
  }
1526
- return /* @__PURE__ */ jsxs5("div", { className: cx3("qt-qb", classNames?.root), children: [
1527
- /* @__PURE__ */ jsxs5("div", { className: "qt-qb-bar qt-qb-state-bar qt-qb-row", children: [
1528
- /* @__PURE__ */ jsxs5("span", { className: "qt-qb-state-main", children: [
1529
- /* @__PURE__ */ jsxs5("span", { className: "qt-qb-saved", children: [
1530
- /* @__PURE__ */ jsx5(
2520
+ async function shareQuery() {
2521
+ if (typeof window === "undefined") return;
2522
+ const url = new URL(window.location.href);
2523
+ url.searchParams.set("q", encodeQuery(api.query));
2524
+ try {
2525
+ await navigator.clipboard.writeText(url.toString());
2526
+ setShareCopied(true);
2527
+ if (shareFeedbackTimerRef.current) clearTimeout(shareFeedbackTimerRef.current);
2528
+ shareFeedbackTimerRef.current = setTimeout(() => {
2529
+ setShareCopied(false);
2530
+ shareFeedbackTimerRef.current = null;
2531
+ }, SHARE_FEEDBACK_MS);
2532
+ } catch {
2533
+ window.prompt("Copy this query URL", url.toString());
2534
+ }
2535
+ }
2536
+ return /* @__PURE__ */ jsxs6("div", { className: cx3("qt-qb", classNames?.root), children: [
2537
+ /* @__PURE__ */ jsxs6("div", { className: "qt-qb-bar qt-qb-state-bar qt-qb-row", children: [
2538
+ /* @__PURE__ */ jsxs6("span", { className: "qt-qb-state-main", children: [
2539
+ /* @__PURE__ */ jsxs6("span", { className: "qt-qb-saved", children: [
2540
+ /* @__PURE__ */ jsx6(
1531
2541
  "button",
1532
2542
  {
1533
2543
  type: "button",
@@ -1538,8 +2548,8 @@ function QueryBuilder({
1538
2548
  children: isSavedQuery ? "\u2605" : "\u2606"
1539
2549
  }
1540
2550
  ),
1541
- isEditingSaved || isEditingUnsaved ? /* @__PURE__ */ jsxs5("span", { className: "qt-qb-saved-input-wrap", children: [
1542
- /* @__PURE__ */ jsx5(
2551
+ isEditingSaved || isEditingUnsaved ? /* @__PURE__ */ jsxs6("span", { className: "qt-qb-saved-input-wrap", children: [
2552
+ /* @__PURE__ */ jsx6(
1543
2553
  "input",
1544
2554
  {
1545
2555
  type: "text",
@@ -1562,7 +2572,7 @@ function QueryBuilder({
1562
2572
  onClick: (e) => e.stopPropagation()
1563
2573
  }
1564
2574
  ),
1565
- /* @__PURE__ */ jsx5(
2575
+ /* @__PURE__ */ jsx6(
1566
2576
  "button",
1567
2577
  {
1568
2578
  type: "button",
@@ -1576,7 +2586,7 @@ function QueryBuilder({
1576
2586
  children: "\u2715"
1577
2587
  }
1578
2588
  )
1579
- ] }) : /* @__PURE__ */ jsx5(
2589
+ ] }) : /* @__PURE__ */ jsx6(
1580
2590
  "button",
1581
2591
  {
1582
2592
  type: "button",
@@ -1586,17 +2596,17 @@ function QueryBuilder({
1586
2596
  }
1587
2597
  )
1588
2598
  ] }),
1589
- isCollapsed ? /* @__PURE__ */ jsxs5("span", { className: "qt-qb-summary qt-truncate", title: collapsedSummary, children: [
1590
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-summary-kw", children: "WHERE" }),
2599
+ isCollapsed ? /* @__PURE__ */ jsxs6("span", { className: "qt-qb-summary qt-truncate", title: collapsedSummary, children: [
2600
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-summary-kw", children: "WHERE" }),
1591
2601
  " ",
1592
- /* @__PURE__ */ jsx5("strong", { children: collapsedWhereText }),
2602
+ /* @__PURE__ */ jsx6("strong", { children: collapsedWhereText }),
1593
2603
  " ",
1594
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-summary-kw", children: "ORDER BY" }),
2604
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-summary-kw", children: "ORDER BY" }),
1595
2605
  " ",
1596
- /* @__PURE__ */ jsx5("strong", { children: collapsedOrderText })
2606
+ /* @__PURE__ */ jsx6("strong", { children: collapsedOrderText })
1597
2607
  ] }) : null
1598
2608
  ] }),
1599
- showUpdateButton ? /* @__PURE__ */ jsx5(
2609
+ showUpdateButton ? /* @__PURE__ */ jsx6(
1600
2610
  "button",
1601
2611
  {
1602
2612
  type: "button",
@@ -1606,11 +2616,22 @@ function QueryBuilder({
1606
2616
  children: `Update Query '${lastSavedQuery?.name ?? "query"}'`
1607
2617
  }
1608
2618
  ) : null,
1609
- /* @__PURE__ */ jsxs5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: () => setShowSaved(true), children: [
2619
+ /* @__PURE__ */ jsx6(
2620
+ "button",
2621
+ {
2622
+ type: "button",
2623
+ className: cx3("qt-btn", classNames?.button),
2624
+ onClick: () => void shareQuery(),
2625
+ title: "Copy a URL for this query",
2626
+ "aria-live": "polite",
2627
+ children: shareCopied ? "Copied!" : "Share"
2628
+ }
2629
+ ),
2630
+ /* @__PURE__ */ jsxs6("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: () => setShowSaved(true), children: [
1610
2631
  "Saved",
1611
2632
  api.saved.items.length > 0 ? ` (${api.saved.items.length})` : ""
1612
2633
  ] }),
1613
- /* @__PURE__ */ jsxs5(
2634
+ /* @__PURE__ */ jsxs6(
1614
2635
  "button",
1615
2636
  {
1616
2637
  type: "button",
@@ -1620,7 +2641,7 @@ function QueryBuilder({
1620
2641
  "aria-controls": bodyId,
1621
2642
  title: isCollapsed ? "Expand query builder" : "Collapse query builder",
1622
2643
  children: [
1623
- /* @__PURE__ */ jsx5(
2644
+ /* @__PURE__ */ jsx6(
1624
2645
  "span",
1625
2646
  {
1626
2647
  className: cx3("qt-qb-collapse-caret", isCollapsed && "qt-qb-collapse-caret--collapsed"),
@@ -1628,38 +2649,38 @@ function QueryBuilder({
1628
2649
  children: "\u25BE"
1629
2650
  }
1630
2651
  ),
1631
- /* @__PURE__ */ jsx5("span", { className: "qt-sr-only", children: isCollapsed ? "Expand query builder" : "Collapse query builder" })
2652
+ /* @__PURE__ */ jsx6("span", { className: "qt-sr-only", children: isCollapsed ? "Expand query builder" : "Collapse query builder" })
1632
2653
  ]
1633
2654
  }
1634
2655
  )
1635
2656
  ] }),
1636
- !isCollapsed ? /* @__PURE__ */ jsxs5("div", { className: "qt-qb-bar qt-qb-editor-bar", children: [
1637
- /* @__PURE__ */ jsxs5("div", { id: bodyId, className: "qt-qb-body", children: [
1638
- /* @__PURE__ */ jsx5(SelectRow, { api, fields, classNames, disabled: isBusy }),
1639
- /* @__PURE__ */ jsx5(WhereRow, { api, byName, fields, classNames, disabled: isBusy }),
1640
- /* @__PURE__ */ jsx5(OrderRow, { api, fields, classNames, disabled: isBusy }),
1641
- /* @__PURE__ */ jsx5(WindowRow, { api, classNames, disabled: isBusy }),
1642
- /* @__PURE__ */ jsx5(MetricsRow, { api, fields, classNames, disabled: isBusy })
2657
+ !isCollapsed ? /* @__PURE__ */ jsxs6("div", { className: "qt-qb-bar qt-qb-editor-bar", children: [
2658
+ /* @__PURE__ */ jsxs6("div", { id: bodyId, className: "qt-qb-body", children: [
2659
+ /* @__PURE__ */ jsx6(SelectRow, { api, fields, classNames, disabled: isBusy }),
2660
+ /* @__PURE__ */ jsx6(WhereRow, { api, byName, fields, classNames, disabled: isBusy }),
2661
+ /* @__PURE__ */ jsx6(OrderRow, { api, fields, classNames, disabled: isBusy }),
2662
+ /* @__PURE__ */ jsx6(WindowRow, { api, classNames, disabled: isBusy }),
2663
+ /* @__PURE__ */ jsx6(MetricsRow, { api, fields, classNames, disabled: isBusy })
1643
2664
  ] }),
1644
- /* @__PURE__ */ jsxs5("div", { className: "qt-qb-editor-stack", children: [
1645
- /* @__PURE__ */ jsx5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.undo, disabled: !api.canUndo || isBusy, children: "\u21B6 Undo" }),
1646
- /* @__PURE__ */ jsx5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.redo, disabled: !api.canRedo || isBusy, children: "\u21B7 Redo" }),
1647
- /* @__PURE__ */ jsx5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.resetAll, disabled: isBusy, children: "Reset All" })
2665
+ /* @__PURE__ */ jsxs6("div", { className: "qt-qb-editor-stack", children: [
2666
+ /* @__PURE__ */ jsx6("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.undo, disabled: !api.canUndo || isBusy, children: "\u21B6 Undo" }),
2667
+ /* @__PURE__ */ jsx6("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.redo, disabled: !api.canRedo || isBusy, children: "\u21B7 Redo" }),
2668
+ /* @__PURE__ */ jsx6("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.resetAll, disabled: isBusy, children: "Reset All" })
1648
2669
  ] })
1649
2670
  ] }) : null,
1650
- /* @__PURE__ */ jsxs5("div", { className: "qt-qb-bar qt-qb-run-bar qt-qb-row", children: [
1651
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-run-metric qt-qb-run-metric--strong", children: api.loading ? "loading\u2026" : `${rowSummary} rows` }),
1652
- /* @__PURE__ */ jsxs5("span", { className: "qt-qb-run-metric", children: [
2671
+ /* @__PURE__ */ jsxs6("div", { className: "qt-qb-bar qt-qb-run-bar qt-qb-row", children: [
2672
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-run-metric qt-qb-run-metric--strong", children: api.loading ? "loading\u2026" : `${rowSummary} rows` }),
2673
+ /* @__PURE__ */ jsxs6("span", { className: "qt-qb-run-metric", children: [
1653
2674
  "Last updated ",
1654
2675
  lastUpdatedText
1655
2676
  ] }),
1656
- failureText ? /* @__PURE__ */ jsx5("span", { className: "qt-qb-run-error", children: failureText }) : null,
1657
- /* @__PURE__ */ jsxs5("span", { className: "qt-qb-run-actions", children: [
1658
- autoRefreshStatus ? /* @__PURE__ */ jsx5("span", { className: "qt-qb-auto-status", children: `Every ${formatDuration2(autoRefreshStatus.frequencyMs)}; clear after ${formatDuration2(
2677
+ failureText ? /* @__PURE__ */ jsx6("span", { className: "qt-qb-run-error", children: failureText }) : null,
2678
+ /* @__PURE__ */ jsxs6("span", { className: "qt-qb-run-actions", children: [
2679
+ autoRefreshStatus ? /* @__PURE__ */ jsx6("span", { className: "qt-qb-auto-status", children: `Every ${formatDuration2(autoRefreshStatus.frequencyMs)}; clear after ${formatDuration2(
1659
2680
  autoRefreshStatus.turnOffAfterMs
1660
2681
  )} (${autoRefreshStatus.pollCount} polls run)` }) : null,
1661
- /* @__PURE__ */ jsxs5("span", { className: "qt-auto-refresh", ref: autoRefreshRef, children: [
1662
- /* @__PURE__ */ jsx5(
2682
+ /* @__PURE__ */ jsxs6("span", { className: "qt-auto-refresh", ref: autoRefreshRef, children: [
2683
+ /* @__PURE__ */ jsx6(
1663
2684
  "button",
1664
2685
  {
1665
2686
  type: "button",
@@ -1670,7 +2691,7 @@ function QueryBuilder({
1670
2691
  children: autoRefreshButtonText
1671
2692
  }
1672
2693
  ),
1673
- showAutoRefresh ? /* @__PURE__ */ jsxs5(
2694
+ showAutoRefresh ? /* @__PURE__ */ jsxs6(
1674
2695
  "span",
1675
2696
  {
1676
2697
  ref: autoRefreshPopoverRef,
@@ -1678,27 +2699,27 @@ function QueryBuilder({
1678
2699
  role: "dialog",
1679
2700
  "aria-label": "Auto-refresh settings",
1680
2701
  children: [
1681
- /* @__PURE__ */ jsxs5("label", { className: "qt-auto-refresh-field", children: [
1682
- /* @__PURE__ */ jsx5("span", { children: "Frequency" }),
1683
- /* @__PURE__ */ jsx5(
2702
+ /* @__PURE__ */ jsxs6("label", { className: "qt-auto-refresh-field", children: [
2703
+ /* @__PURE__ */ jsx6("span", { children: "Frequency" }),
2704
+ /* @__PURE__ */ jsx6(
1684
2705
  "select",
1685
2706
  {
1686
2707
  className: "qt-auto-refresh-select",
1687
2708
  value: autoRefreshFrequencyMs,
1688
2709
  onChange: (e) => setAutoRefreshFrequencyMs(Number(e.target.value)),
1689
- children: AUTO_REFRESH_FREQUENCIES.map((option) => /* @__PURE__ */ jsx5("option", { value: option.value, children: option.label }, option.value))
2710
+ children: AUTO_REFRESH_FREQUENCIES.map((option) => /* @__PURE__ */ jsx6("option", { value: option.value, children: option.label }, option.value))
1690
2711
  }
1691
2712
  )
1692
2713
  ] }),
1693
- /* @__PURE__ */ jsxs5("label", { className: "qt-auto-refresh-field", children: [
1694
- /* @__PURE__ */ jsx5("span", { children: "Turn off after" }),
1695
- /* @__PURE__ */ jsx5(
2714
+ /* @__PURE__ */ jsxs6("label", { className: "qt-auto-refresh-field", children: [
2715
+ /* @__PURE__ */ jsx6("span", { children: "Turn off after" }),
2716
+ /* @__PURE__ */ jsx6(
1696
2717
  "select",
1697
2718
  {
1698
2719
  className: "qt-auto-refresh-select",
1699
2720
  value: autoRefreshTurnOffAfterMs,
1700
2721
  onChange: (e) => setAutoRefreshTurnOffAfterMs(Number(e.target.value)),
1701
- children: AUTO_REFRESH_TURN_OFF_AFTER.map((option) => /* @__PURE__ */ jsx5(
2722
+ children: AUTO_REFRESH_TURN_OFF_AFTER.map((option) => /* @__PURE__ */ jsx6(
1702
2723
  "option",
1703
2724
  {
1704
2725
  value: option.value,
@@ -1710,8 +2731,8 @@ function QueryBuilder({
1710
2731
  }
1711
2732
  )
1712
2733
  ] }),
1713
- /* @__PURE__ */ jsx5("span", { className: cx3("qt-auto-refresh-status", !canSubmitAutoRefresh && "qt-auto-refresh-status--error"), children: canSubmitAutoRefresh ? `${selectedAutoRefreshPolls} polls maximum` : `Choose between 1 and 1000 polls` }),
1714
- autoRefreshStatus ? /* @__PURE__ */ jsxs5("span", { className: "qt-auto-refresh-status", children: [
2734
+ /* @__PURE__ */ jsx6("span", { className: cx3("qt-auto-refresh-status", !canSubmitAutoRefresh && "qt-auto-refresh-status--error"), children: canSubmitAutoRefresh ? `${selectedAutoRefreshPolls} polls maximum` : `Choose between 1 and 1000 polls` }),
2735
+ autoRefreshStatus ? /* @__PURE__ */ jsxs6("span", { className: "qt-auto-refresh-status", children: [
1715
2736
  "Active: every ",
1716
2737
  formatDuration2(autoRefreshStatus.frequencyMs),
1717
2738
  ", clears after",
@@ -1721,8 +2742,8 @@ function QueryBuilder({
1721
2742
  autoRefreshStatus.pollCount,
1722
2743
  " polls run."
1723
2744
  ] }) : null,
1724
- /* @__PURE__ */ jsxs5("span", { className: "qt-auto-refresh-actions", children: [
1725
- /* @__PURE__ */ jsx5(
2745
+ /* @__PURE__ */ jsxs6("span", { className: "qt-auto-refresh-actions", children: [
2746
+ /* @__PURE__ */ jsx6(
1726
2747
  "button",
1727
2748
  {
1728
2749
  type: "button",
@@ -1732,7 +2753,7 @@ function QueryBuilder({
1732
2753
  children: autoRefreshStatus ? "Update" : "Start"
1733
2754
  }
1734
2755
  ),
1735
- autoRefreshStatus ? /* @__PURE__ */ jsx5(
2756
+ autoRefreshStatus ? /* @__PURE__ */ jsx6(
1736
2757
  "button",
1737
2758
  {
1738
2759
  type: "button",
@@ -1749,7 +2770,7 @@ function QueryBuilder({
1749
2770
  }
1750
2771
  ) : null
1751
2772
  ] }),
1752
- /* @__PURE__ */ jsx5(
2773
+ /* @__PURE__ */ jsx6(
1753
2774
  "button",
1754
2775
  {
1755
2776
  type: "button",
@@ -1761,7 +2782,42 @@ function QueryBuilder({
1761
2782
  )
1762
2783
  ] })
1763
2784
  ] }),
1764
- !isCollapsed && showSaved ? /* @__PURE__ */ jsx5(SavedQueriesModal, { saved: api.saved, onClose: () => setShowSaved(false), onLoad: setLastSavedId }) : null
2785
+ !isCollapsed && showSaved ? /* @__PURE__ */ jsx6(SavedQueriesModal, { saved: api.saved, onClose: () => setShowSaved(false), onLoad: setLastSavedId }) : null
2786
+ ] });
2787
+ }
2788
+ function MoveButtons({
2789
+ label,
2790
+ position,
2791
+ count,
2792
+ disabled,
2793
+ onMove
2794
+ }) {
2795
+ if (count < 2) return null;
2796
+ return /* @__PURE__ */ jsxs6("span", { className: "qt-chip-move-buttons", children: [
2797
+ /* @__PURE__ */ jsx6(
2798
+ "button",
2799
+ {
2800
+ type: "button",
2801
+ className: "qt-chip-move",
2802
+ disabled: disabled || position === 0,
2803
+ onClick: () => onMove(position - 1),
2804
+ "aria-label": `Move ${label} earlier`,
2805
+ title: "Move earlier",
2806
+ children: "\u2190"
2807
+ }
2808
+ ),
2809
+ /* @__PURE__ */ jsx6(
2810
+ "button",
2811
+ {
2812
+ type: "button",
2813
+ className: "qt-chip-move",
2814
+ disabled: disabled || position === count - 1,
2815
+ onClick: () => onMove(position + 1),
2816
+ "aria-label": `Move ${label} later`,
2817
+ title: "Move later",
2818
+ children: "\u2192"
2819
+ }
2820
+ )
1765
2821
  ] });
1766
2822
  }
1767
2823
  function SelectRow({
@@ -1771,19 +2827,22 @@ function SelectRow({
1771
2827
  disabled
1772
2828
  }) {
1773
2829
  const { select, columnDrag } = api;
1774
- const [adding, setAdding] = useState4(false);
1775
- const fieldLabelByName = useMemo3(() => new Map(select.fields.map((f) => [f.name, f.label])), [select.fields]);
2830
+ const [editingColumns, setEditingColumns] = useState5(false);
2831
+ const [adding, setAdding] = useState5(false);
2832
+ const fieldLabelByName = useMemo4(() => new Map(select.fields.map((f) => [f.name, f.label])), [select.fields]);
1776
2833
  function reorderByIndex(from, overIndex) {
1777
2834
  select.move(from, overIndex);
1778
2835
  }
1779
2836
  const fieldNames = select.visible.map((c) => c.field);
1780
2837
  const dragSource = columnDrag.source;
1781
2838
  const rendered = columnDrag.preview(fieldNames);
1782
- return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row", children: [
1783
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "select" }),
1784
- rendered.map((name) => {
2839
+ return /* @__PURE__ */ jsxs6("div", { className: "qt-qb-row", children: [
2840
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-kw", children: "select" }),
2841
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-btn", disabled, onClick: () => setEditingColumns(true), children: "Edit columns" }),
2842
+ editingColumns && /* @__PURE__ */ jsx6(SelectColumnEditor, { api, onClose: () => setEditingColumns(false) }),
2843
+ rendered.map((name, position) => {
1785
2844
  const label = fieldLabelByName.get(name) ?? name;
1786
- return /* @__PURE__ */ jsxs5(
2845
+ return /* @__PURE__ */ jsxs6(
1787
2846
  "span",
1788
2847
  {
1789
2848
  className: cx3(
@@ -1822,25 +2881,35 @@ function SelectRow({
1822
2881
  onDragEnd: () => columnDrag.end(),
1823
2882
  title: "drag to reorder",
1824
2883
  children: [
1825
- /* @__PURE__ */ jsx5("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
1826
- /* @__PURE__ */ jsx5("span", { className: "qt-chip-field", children: label }),
1827
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: () => select.hide(name), disabled, children: "\u2715" })
2884
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
2885
+ /* @__PURE__ */ jsx6("span", { className: "qt-chip-field", children: label }),
2886
+ /* @__PURE__ */ jsx6(
2887
+ MoveButtons,
2888
+ {
2889
+ label,
2890
+ position,
2891
+ count: rendered.length,
2892
+ disabled,
2893
+ onMove: (nextPosition) => select.move(name, nextPosition)
2894
+ }
2895
+ ),
2896
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-chip-x", onClick: () => select.hide(name), disabled, children: "\u2715" })
1828
2897
  ]
1829
2898
  },
1830
2899
  name
1831
2900
  );
1832
2901
  }),
1833
- adding ? /* @__PURE__ */ jsx5(
2902
+ adding ? /* @__PURE__ */ jsx6(
1834
2903
  FieldPicker,
1835
2904
  {
1836
- fields: select.hidden.filter(isSelectable),
2905
+ fields: select.hidden.filter(isSelectable2),
1837
2906
  onPick: (f) => {
1838
2907
  select.show(f.name);
1839
2908
  setAdding(false);
1840
2909
  },
1841
2910
  onClose: () => setAdding(false)
1842
2911
  }
1843
- ) : /* @__PURE__ */ jsx5(
2912
+ ) : /* @__PURE__ */ jsx6(
1844
2913
  "button",
1845
2914
  {
1846
2915
  type: "button",
@@ -1850,9 +2919,16 @@ function SelectRow({
1850
2919
  children: "+ add column"
1851
2920
  }
1852
2921
  ),
1853
- api.query.select.length > 0 && /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-link-btn", onClick: select.reset, disabled, title: "Reset to default columns", children: "reset" })
2922
+ api.query.select.length > 0 && /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-link-btn", onClick: select.reset, disabled, title: "Reset to default columns", children: "reset" })
1854
2923
  ] });
1855
2924
  }
2925
+ function dropModeFor(e) {
2926
+ const rect = e.currentTarget.getBoundingClientRect();
2927
+ const x = e.clientX - rect.left;
2928
+ if (x < rect.width * 0.3) return "before";
2929
+ if (x > rect.width * 0.7) return "after";
2930
+ return "merge";
2931
+ }
1856
2932
  function WhereRow({
1857
2933
  api,
1858
2934
  byName,
@@ -1860,120 +2936,295 @@ function WhereRow({
1860
2936
  classNames,
1861
2937
  disabled
1862
2938
  }) {
1863
- const [adding, setAdding] = useState4(false);
2939
+ const [adding, setAdding] = useState5(false);
2940
+ const [drag, setDrag] = useState5(null);
2941
+ const where = api.query.where;
2942
+ const hasOr = where.some(isOrGroup);
1864
2943
  function addClause(f) {
1865
- const op = opsForField2(f)[0] ?? "=";
2944
+ const op = positiveOpsForField(f)[0] ?? opsForField2(f)[0] ?? "=";
1866
2945
  api.addFilter({ field: f.name, op, value: "" });
1867
2946
  setAdding(false);
1868
2947
  }
1869
- const groupedWhere = useMemo3(() => {
1870
- const groups = /* @__PURE__ */ new Map();
1871
- const order = [];
1872
- api.query.where.forEach((c, i) => {
1873
- if (!groups.has(c.field)) order.push(c.field);
1874
- const group = groups.get(c.field) ?? { field: c.field, clauses: [] };
1875
- group.clauses.push({ clause: c, index: i });
1876
- groups.set(c.field, group);
1877
- });
1878
- return order.map((fieldName) => groups.get(fieldName));
1879
- }, [api.query.where]);
1880
- return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row", children: [
1881
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "where" }),
1882
- api.query.where.length === 0 && !adding && /* @__PURE__ */ jsx5("span", { className: "qt-qb-hint", children: "all rows" }),
1883
- groupedWhere.map((g) => /* @__PURE__ */ jsx5(
1884
- ClauseChip,
1885
- {
1886
- api,
1887
- field: byName.get(g.field),
1888
- clauses: g.clauses,
1889
- classNames,
1890
- disabled,
1891
- onChange: (index, next) => api.updateFilter(index, next),
1892
- onRemove: (index) => api.removeFilter(index)
1893
- },
1894
- g.field
1895
- )),
1896
- adding ? /* @__PURE__ */ jsx5(
2948
+ function handleOver(e, index) {
2949
+ if (!drag) return;
2950
+ e.preventDefault();
2951
+ e.dataTransfer.dropEffect = "move";
2952
+ const mode = dropModeFor(e);
2953
+ setDrag((d) => d && (d.over?.index !== index || d.over?.mode !== mode) ? { from: d.from, over: { index, mode } } : d);
2954
+ }
2955
+ function handleDrop() {
2956
+ if (!drag || !drag.over) {
2957
+ setDrag(null);
2958
+ return;
2959
+ }
2960
+ const { from } = drag;
2961
+ const { index: target, mode } = drag.over;
2962
+ if (mode === "merge") {
2963
+ if (target !== from) api.mergeFilters(from, target);
2964
+ } else {
2965
+ const tPrime = target < from ? target : target - 1;
2966
+ const insertAt = mode === "after" ? tPrime + 1 : tPrime;
2967
+ if (insertAt !== from) api.reorderFilters(from, insertAt);
2968
+ }
2969
+ setDrag(null);
2970
+ }
2971
+ return /* @__PURE__ */ jsxs6("div", { className: "qt-qb-row", children: [
2972
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-kw", children: "where" }),
2973
+ where.length === 0 && !adding && /* @__PURE__ */ jsx6("span", { className: "qt-qb-hint", children: "all rows" }),
2974
+ where.map((term, index) => /* @__PURE__ */ jsxs6("span", { className: "qt-where-term-wrap", children: [
2975
+ index > 0 && hasOr && /* @__PURE__ */ jsx6("span", { className: "qt-where-and", children: "AND" }),
2976
+ /* @__PURE__ */ jsx6(
2977
+ TermChip,
2978
+ {
2979
+ api,
2980
+ term,
2981
+ index,
2982
+ byName,
2983
+ classNames,
2984
+ disabled,
2985
+ isDragSource: drag?.from === index,
2986
+ dropHint: drag?.over?.index === index ? drag.over.mode : null,
2987
+ onDragStart: () => setDrag({ from: index, over: null }),
2988
+ onDragOver: (e) => handleOver(e, index),
2989
+ onDrop: handleDrop,
2990
+ onDragEnd: () => setDrag(null)
2991
+ }
2992
+ )
2993
+ ] }, index)),
2994
+ adding ? /* @__PURE__ */ jsx6(
1897
2995
  FieldPicker,
1898
2996
  {
1899
- fields: fields.filter(isFilterable),
2997
+ fields: fields.filter(isFilterable2),
1900
2998
  gateOnDistinct: true,
1901
2999
  onPick: addClause,
1902
3000
  onClose: () => setAdding(false)
1903
3001
  }
1904
- ) : /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-add", onClick: () => setAdding(true), disabled, children: "+ add filter" }),
1905
- api.query.where.length > 0 && /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-link-btn", onClick: api.clearFilters, disabled, title: "Reset filters", children: "reset" })
3002
+ ) : /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-add", onClick: () => setAdding(true), disabled, children: "+ add filter" }),
3003
+ where.length > 0 && /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-link-btn", onClick: api.clearFilters, disabled, title: "Reset filters", children: "reset" })
1906
3004
  ] });
1907
3005
  }
1908
- function ClauseChip({
3006
+ function TermChip({
1909
3007
  api,
1910
- field,
1911
- clauses,
3008
+ term,
3009
+ index,
3010
+ byName,
1912
3011
  classNames,
1913
3012
  disabled,
1914
- onChange,
1915
- onRemove
3013
+ isDragSource,
3014
+ dropHint,
3015
+ onDragStart,
3016
+ onDragOver,
3017
+ onDrop,
3018
+ onDragEnd
1916
3019
  }) {
1917
- const labelField = field?.label;
1918
- const fieldName = clauses[0]?.clause.field;
1919
- const fieldHasNull = useFieldHasNull(api, field?.name);
1920
- return /* @__PURE__ */ jsxs5("span", { className: cx3("qt-chip", "qt-chip--where", classNames?.chip), children: [
1921
- /* @__PURE__ */ jsx5("span", { className: "qt-chip-field", children: labelField ?? fieldName }),
1922
- /* @__PURE__ */ jsx5("span", { className: "qt-chip-where-list", children: clauses.map((entry, idx) => {
1923
- const clause = entry.clause;
1924
- const baseOps = field ? opsForField2(field) : [clause.op];
1925
- const filteredBaseOps = fieldHasNull === false ? baseOps.filter((op) => !NULLARY_OPS2.has(op)) : baseOps;
1926
- const clauseOps = filteredBaseOps.includes(clause.op) ? filteredBaseOps : [...filteredBaseOps, clause.op];
1927
- const needsValue = !NULLARY_OPS2.has(clause.op);
1928
- return /* @__PURE__ */ jsxs5("span", { className: "qt-chip-where-clause", children: [
1929
- idx > 0 && /* @__PURE__ */ jsx5("span", { className: "qt-chip-and", children: "and" }),
1930
- /* @__PURE__ */ jsx5(
1931
- "select",
3020
+ const group = isOrGroup(term);
3021
+ const predicates = predicatesOf(term);
3022
+ const grip = /* @__PURE__ */ jsx6(
3023
+ "span",
3024
+ {
3025
+ className: "qt-chip-grip",
3026
+ "aria-hidden": true,
3027
+ draggable: !disabled,
3028
+ onDragStart: (e) => {
3029
+ onDragStart();
3030
+ e.dataTransfer.effectAllowed = "move";
3031
+ e.dataTransfer.setData("text/plain", String(index));
3032
+ },
3033
+ onDragEnd,
3034
+ title: "drag to reorder, or drop onto another filter to make an OR",
3035
+ children: "\u22EE\u22EE"
3036
+ }
3037
+ );
3038
+ return /* @__PURE__ */ jsxs6(
3039
+ "span",
3040
+ {
3041
+ className: cx3(
3042
+ "qt-chip",
3043
+ "qt-chip--where",
3044
+ group && "qt-chip--or",
3045
+ isDragSource && "qt-chip--dragging",
3046
+ dropHint ? `qt-chip--drop-${dropHint}` : void 0,
3047
+ classNames?.chip
3048
+ ),
3049
+ onDragOver,
3050
+ onDrop: (e) => {
3051
+ e.preventDefault();
3052
+ onDrop();
3053
+ },
3054
+ children: [
3055
+ grip,
3056
+ group ? /* @__PURE__ */ jsx6("span", { className: "qt-or-group", children: predicates.map((clause, predIndex) => /* @__PURE__ */ jsxs6("span", { className: "qt-or-member", children: [
3057
+ predIndex > 0 && /* @__PURE__ */ jsx6("span", { className: "qt-chip-and qt-chip-or", children: "OR" }),
3058
+ /* @__PURE__ */ jsx6(
3059
+ PredicateEditor,
3060
+ {
3061
+ api,
3062
+ field: byName.get(clause.field),
3063
+ clause,
3064
+ classNames,
3065
+ disabled,
3066
+ onChange: (next) => api.updatePredicate(index, predIndex, next),
3067
+ onRemove: () => api.removePredicate(index, predIndex)
3068
+ }
3069
+ )
3070
+ ] }, predIndex)) }) : /* @__PURE__ */ jsx6(
3071
+ PredicateEditor,
1932
3072
  {
1933
- className: cx3("qt-chip-op", classNames?.select),
1934
- value: clause.op,
3073
+ api,
3074
+ field: byName.get(term.field),
3075
+ clause: term,
3076
+ classNames,
1935
3077
  disabled,
1936
- style: { width: `${chipFieldWidthForSelect(clause.op, 3)}ch` },
1937
- onChange: (e) => onChange(entry.index, { ...clause, op: e.target.value }),
1938
- children: clauseOps.map((op) => /* @__PURE__ */ jsx5("option", { value: op, children: op }, op))
3078
+ onChange: (next) => api.updatePredicate(index, 0, next),
3079
+ onRemove: () => api.removeFilter(index)
1939
3080
  }
1940
- ),
1941
- needsValue && /* @__PURE__ */ jsx5(
1942
- ValueInput,
3081
+ )
3082
+ ]
3083
+ }
3084
+ );
3085
+ }
3086
+ var OP_LABEL = {
3087
+ "=": "=",
3088
+ "!=": "\u2260",
3089
+ ">": ">",
3090
+ ">=": "\u2265",
3091
+ "<": "<",
3092
+ "<=": "\u2264",
3093
+ contains: "contains",
3094
+ starts_with: "starts with",
3095
+ ends_with: "ends with",
3096
+ matches_regex: "matches",
3097
+ not_matches_regex: "not matches",
3098
+ includes: "includes",
3099
+ is_null: "is null",
3100
+ is_not_null: "is not null"
3101
+ };
3102
+ function choiceLabel(op, negated) {
3103
+ return negated ? `not ${OP_LABEL[op]}` : OP_LABEL[op];
3104
+ }
3105
+ function OpPicker({
3106
+ field,
3107
+ clause,
3108
+ disabled,
3109
+ classNames,
3110
+ onPick
3111
+ }) {
3112
+ const [open, setOpen] = useState5(false);
3113
+ const wrapRef = useRef3(null);
3114
+ useEffect5(() => {
3115
+ if (!open) return;
3116
+ function onDocClick(e) {
3117
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
3118
+ }
3119
+ function onKey(e) {
3120
+ if (e.key === "Escape") setOpen(false);
3121
+ }
3122
+ const t = setTimeout(() => window.addEventListener("click", onDocClick), 0);
3123
+ window.addEventListener("keydown", onKey);
3124
+ return () => {
3125
+ clearTimeout(t);
3126
+ window.removeEventListener("click", onDocClick);
3127
+ window.removeEventListener("keydown", onKey);
3128
+ };
3129
+ }, [open]);
3130
+ const pairs = useMemo4(() => field ? opPairsForField2(field) : [], [field]);
3131
+ const isActive = (choice) => choice.op === clause.op && Boolean(choice.negated) === Boolean(clause.negated);
3132
+ return /* @__PURE__ */ jsxs6("span", { className: "qt-op-picker", ref: wrapRef, children: [
3133
+ /* @__PURE__ */ jsxs6(
3134
+ "button",
3135
+ {
3136
+ type: "button",
3137
+ className: cx3("qt-chip-op-btn", "qt-op-trigger", classNames?.select),
3138
+ disabled,
3139
+ "aria-haspopup": "dialog",
3140
+ "aria-expanded": open,
3141
+ onClick: () => setOpen((o) => !o),
3142
+ children: [
3143
+ choiceLabel(clause.op, Boolean(clause.negated)),
3144
+ /* @__PURE__ */ jsx6("span", { className: "qt-op-caret", "aria-hidden": true, children: "\u25BE" })
3145
+ ]
3146
+ }
3147
+ ),
3148
+ open && pairs.length > 0 && /* @__PURE__ */ jsxs6("span", { className: "qt-op-pop", role: "dialog", "aria-label": "Choose condition", children: [
3149
+ /* @__PURE__ */ jsx6("span", { className: "qt-cm-col-head", children: "keep" }),
3150
+ /* @__PURE__ */ jsx6("span", { className: "qt-cm-col-head qt-cm-col-head--neg", children: "exclude" }),
3151
+ pairs.map((pair, i) => /* @__PURE__ */ jsxs6(Fragment5, { children: [
3152
+ /* @__PURE__ */ jsx6(
3153
+ "button",
1943
3154
  {
1944
- api,
1945
- field,
1946
- value: clause.value,
1947
- classNames,
1948
- onChange: (v) => onChange(entry.index, { ...clause, value: v })
3155
+ type: "button",
3156
+ className: cx3("qt-op-cell", isActive(pair.keep) && "qt-op-cell--on"),
3157
+ onClick: () => {
3158
+ onPick(pair.keep);
3159
+ setOpen(false);
3160
+ },
3161
+ children: choiceLabel(pair.keep.op, pair.keep.negated)
1949
3162
  }
1950
3163
  ),
1951
- /* @__PURE__ */ jsx5(
3164
+ /* @__PURE__ */ jsx6(
1952
3165
  "button",
1953
3166
  {
1954
3167
  type: "button",
1955
- className: "qt-chip-x",
1956
- onClick: () => onRemove(entry.index),
1957
- disabled,
1958
- children: "\u2715"
3168
+ className: cx3("qt-op-cell", "qt-op-cell--neg", isActive(pair.exclude) && "qt-op-cell--on"),
3169
+ onClick: () => {
3170
+ onPick(pair.exclude);
3171
+ setOpen(false);
3172
+ },
3173
+ children: choiceLabel(pair.exclude.op, pair.exclude.negated)
1959
3174
  }
1960
3175
  )
1961
- ] }, entry.index);
1962
- }) })
3176
+ ] }, i))
3177
+ ] })
3178
+ ] });
3179
+ }
3180
+ function PredicateEditor({
3181
+ api,
3182
+ field,
3183
+ clause,
3184
+ classNames,
3185
+ disabled,
3186
+ onChange,
3187
+ onRemove
3188
+ }) {
3189
+ const needsValue = !NULLARY_OPS2.has(clause.op);
3190
+ const isRegex = clause.op === "matches_regex" || clause.op === "not_matches_regex";
3191
+ function pickOp(choice) {
3192
+ const next = { field: clause.field, op: choice.op, value: clause.value };
3193
+ if (choice.negated) next.negated = true;
3194
+ onChange(next);
3195
+ }
3196
+ return /* @__PURE__ */ jsxs6("span", { className: "qt-pred", children: [
3197
+ /* @__PURE__ */ jsx6("span", { className: "qt-chip-field", children: field?.label ?? clause.field }),
3198
+ /* @__PURE__ */ jsx6(OpPicker, { field, clause, disabled, classNames, onPick: pickOp }),
3199
+ needsValue && /* @__PURE__ */ jsx6(
3200
+ ValueInput,
3201
+ {
3202
+ api,
3203
+ field,
3204
+ value: clause.value,
3205
+ forceFreeform: isRegex,
3206
+ placeholder: isRegex ? "regex" : "value",
3207
+ classNames,
3208
+ onChange: (v) => onChange({ ...clause, value: v })
3209
+ }
3210
+ ),
3211
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-chip-x", onClick: onRemove, disabled, children: "\u2715" })
1963
3212
  ] });
1964
3213
  }
1965
3214
  function ValueInput({
1966
3215
  api,
1967
3216
  field,
1968
3217
  value,
3218
+ forceFreeform,
3219
+ placeholder = "value",
1969
3220
  classNames,
1970
3221
  onChange
1971
3222
  }) {
1972
3223
  const strategy = field ? filterValuesFor(field) : { source: "freeform" };
1973
- if (strategy.source === "static") {
3224
+ if (!forceFreeform && strategy.source === "static") {
1974
3225
  const selectedText = value || "\u2014";
1975
3226
  const widthChars = chipFieldWidthForSelect(selectedText, 4);
1976
- return /* @__PURE__ */ jsxs5(
3227
+ return /* @__PURE__ */ jsxs6(
1977
3228
  "select",
1978
3229
  {
1979
3230
  className: cx3("qt-chip-val", classNames?.select),
@@ -1981,27 +3232,27 @@ function ValueInput({
1981
3232
  style: { width: `${widthChars}ch` },
1982
3233
  onChange: (e) => onChange(e.target.value),
1983
3234
  children: [
1984
- /* @__PURE__ */ jsx5("option", { value: "", children: "\u2014" }),
1985
- strategy.options.map((o) => /* @__PURE__ */ jsx5("option", { value: o, children: o }, o))
3235
+ /* @__PURE__ */ jsx6("option", { value: "", children: "\u2014" }),
3236
+ strategy.options.map((o) => /* @__PURE__ */ jsx6("option", { value: o, children: o }, o))
1986
3237
  ]
1987
3238
  }
1988
3239
  );
1989
3240
  }
1990
- if (strategy.source === "freeform" || !field) {
1991
- const widthChars = chipFieldWidthForInput(value || "value", field?.type, 5, 4);
1992
- return /* @__PURE__ */ jsx5(
3241
+ if (forceFreeform || strategy.source === "freeform" || !field) {
3242
+ const widthChars = chipFieldWidthForInput(value || placeholder, field?.type, 5, 4);
3243
+ return /* @__PURE__ */ jsx6(
1993
3244
  "input",
1994
3245
  {
1995
3246
  className: cx3("qt-chip-val", classNames?.input),
1996
3247
  value,
1997
- placeholder: "value",
3248
+ placeholder,
1998
3249
  size: widthChars,
1999
3250
  style: { width: `${widthChars}ch` },
2000
3251
  onChange: (e) => onChange(e.target.value)
2001
3252
  }
2002
3253
  );
2003
3254
  }
2004
- return /* @__PURE__ */ jsx5(AutocompleteInput, { api, field, value, classNames, onChange });
3255
+ return /* @__PURE__ */ jsx6(AutocompleteInput, { api, field, value, classNames, onChange });
2005
3256
  }
2006
3257
  function AutocompleteInput({
2007
3258
  api,
@@ -2010,9 +3261,9 @@ function AutocompleteInput({
2010
3261
  classNames,
2011
3262
  onChange
2012
3263
  }) {
2013
- const [result, setResult] = useState4({ values: [], hasMore: false });
2014
- const listId = useId();
2015
- useEffect4(() => {
3264
+ const [result, setResult] = useState5({ values: [], hasMore: false });
3265
+ const listId = useId2();
3266
+ useEffect5(() => {
2016
3267
  let cancelled = false;
2017
3268
  const t = setTimeout(() => {
2018
3269
  void api.filterValues(field.name, value).then((r) => {
@@ -2026,8 +3277,8 @@ function AutocompleteInput({
2026
3277
  clearTimeout(t);
2027
3278
  };
2028
3279
  }, [api, field.name, value]);
2029
- return /* @__PURE__ */ jsxs5("span", { className: "qt-autocomplete", children: [
2030
- /* @__PURE__ */ jsx5(
3280
+ return /* @__PURE__ */ jsxs6("span", { className: "qt-autocomplete", children: [
3281
+ /* @__PURE__ */ jsx6(
2031
3282
  "input",
2032
3283
  {
2033
3284
  className: cx3("qt-chip-val", classNames?.input),
@@ -2039,8 +3290,8 @@ function AutocompleteInput({
2039
3290
  onChange: (e) => onChange(e.target.value)
2040
3291
  }
2041
3292
  ),
2042
- /* @__PURE__ */ jsx5("datalist", { id: listId, children: result.values.map((v) => /* @__PURE__ */ jsx5("option", { value: v }, v)) }),
2043
- result.hasMore && /* @__PURE__ */ jsx5("span", { className: "qt-autocomplete-hint", children: "keep typing to refine\u2026" })
3293
+ /* @__PURE__ */ jsx6("datalist", { id: listId, children: result.values.map((v) => /* @__PURE__ */ jsx6("option", { value: v }, v)) }),
3294
+ result.hasMore && /* @__PURE__ */ jsx6("span", { className: "qt-autocomplete-hint", children: "keep typing to refine\u2026" })
2044
3295
  ] });
2045
3296
  }
2046
3297
  function OrderTermChip({
@@ -2057,12 +3308,16 @@ function OrderTermChip({
2057
3308
  onDragEnd,
2058
3309
  disabled,
2059
3310
  updateTerm,
3311
+ clearExtract,
2060
3312
  removeTerm,
3313
+ position,
3314
+ count,
3315
+ moveTerm,
2061
3316
  classNames,
2062
3317
  label
2063
3318
  }) {
2064
3319
  const fieldHasNull = useFieldHasNull(api, field?.name);
2065
- return /* @__PURE__ */ jsxs5(
3320
+ return /* @__PURE__ */ jsxs6(
2066
3321
  "span",
2067
3322
  {
2068
3323
  className: cx3("qt-chip", "qt-chip--col", "qt-chip--sort", isDragSource && "qt-chip--dragging", classNames?.chip),
@@ -2080,10 +3335,10 @@ function OrderTermChip({
2080
3335
  onDragEnd,
2081
3336
  title: "drag to reorder sort priority",
2082
3337
  children: [
2083
- /* @__PURE__ */ jsx5("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
2084
- showPriority && /* @__PURE__ */ jsx5("span", { className: "qt-chip-priority", children: priority }),
2085
- /* @__PURE__ */ jsx5("span", { className: "qt-chip-field", children: label }),
2086
- /* @__PURE__ */ jsx5(
3338
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
3339
+ showPriority && /* @__PURE__ */ jsx6("span", { className: "qt-chip-priority", children: priority }),
3340
+ /* @__PURE__ */ jsx6("span", { className: "qt-chip-field", children: label }),
3341
+ /* @__PURE__ */ jsx6(
2087
3342
  "button",
2088
3343
  {
2089
3344
  type: "button",
@@ -2093,7 +3348,45 @@ function OrderTermChip({
2093
3348
  children: term.dir === "asc" ? "\u2191 asc" : "\u2193 desc"
2094
3349
  }
2095
3350
  ),
2096
- fieldHasNull !== false && /* @__PURE__ */ jsxs5(
3351
+ term.extract ? /* @__PURE__ */ jsxs6("span", { className: "qt-sort-extract", children: [
3352
+ /* @__PURE__ */ jsx6("span", { className: "qt-chip-and", children: "extract" }),
3353
+ /* @__PURE__ */ jsx6(
3354
+ "input",
3355
+ {
3356
+ className: cx3("qt-chip-val", classNames?.input),
3357
+ value: term.extract.regex,
3358
+ placeholder: "regex",
3359
+ "aria-label": `Regex extract for ${label}`,
3360
+ size: chipFieldWidthForInput(term.extract.regex || "regex", "text", 5, 4),
3361
+ style: { width: `${chipFieldWidthForInput(term.extract.regex || "regex", "text", 5, 4)}ch` },
3362
+ disabled,
3363
+ onChange: (e) => updateTerm(dataIndex, { extract: { regex: e.target.value } })
3364
+ }
3365
+ ),
3366
+ /* @__PURE__ */ jsx6(
3367
+ "button",
3368
+ {
3369
+ type: "button",
3370
+ className: "qt-chip-x",
3371
+ title: "Remove regex extraction",
3372
+ "aria-label": `Remove regex extract for ${label}`,
3373
+ disabled,
3374
+ onClick: () => clearExtract(dataIndex),
3375
+ children: "\u2715"
3376
+ }
3377
+ )
3378
+ ] }) : /* @__PURE__ */ jsx6(
3379
+ "button",
3380
+ {
3381
+ type: "button",
3382
+ className: "qt-chip-op-btn",
3383
+ title: "Sort by text extracted with a regular expression",
3384
+ disabled,
3385
+ onClick: () => updateTerm(dataIndex, { extract: { regex: "" } }),
3386
+ children: "+ regex extract"
3387
+ }
3388
+ ),
3389
+ (term.extract != null || fieldHasNull !== false) && /* @__PURE__ */ jsxs6(
2097
3390
  "button",
2098
3391
  {
2099
3392
  type: "button",
@@ -2107,7 +3400,17 @@ function OrderTermChip({
2107
3400
  ]
2108
3401
  }
2109
3402
  ),
2110
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: () => removeTerm(dataIndex), disabled, children: "\u2715" })
3403
+ /* @__PURE__ */ jsx6(
3404
+ MoveButtons,
3405
+ {
3406
+ label,
3407
+ position,
3408
+ count,
3409
+ disabled,
3410
+ onMove: moveTerm
3411
+ }
3412
+ ),
3413
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-chip-x", onClick: () => removeTerm(dataIndex), disabled, children: "\u2715" })
2111
3414
  ]
2112
3415
  }
2113
3416
  );
@@ -2119,15 +3422,24 @@ function OrderRow({
2119
3422
  disabled
2120
3423
  }) {
2121
3424
  const orderBy = api.query.orderBy;
2122
- const byName = useMemo3(() => new Map(fields.map((f) => [f.name, f])), [fields]);
2123
- const [drag, setDrag] = useState4(null);
2124
- const [adding, setAdding] = useState4(false);
3425
+ const byName = useMemo4(() => new Map(fields.map((f) => [f.name, f])), [fields]);
3426
+ const [drag, setDrag] = useState5(null);
3427
+ const [adding, setAdding] = useState5(false);
2125
3428
  function setOrderBy(next) {
2126
3429
  api.setSort(next);
2127
3430
  }
2128
3431
  function updateTerm(i, patch) {
2129
3432
  setOrderBy(orderBy.map((o, k) => k === i ? { ...o, ...patch } : o));
2130
3433
  }
3434
+ function clearExtract(i) {
3435
+ setOrderBy(
3436
+ orderBy.map((term, index) => {
3437
+ if (index !== i) return term;
3438
+ const { extract: _extract, ...plainTerm } = term;
3439
+ return plainTerm;
3440
+ })
3441
+ );
3442
+ }
2131
3443
  function removeTerm(i) {
2132
3444
  setOrderBy(orderBy.filter((_, k) => k !== i));
2133
3445
  }
@@ -2153,7 +3465,7 @@ function OrderRow({
2153
3465
  setAdding(false);
2154
3466
  }
2155
3467
  const termFields = orderBy.map((o) => o.field);
2156
- const termByField = useMemo3(() => new Map(orderBy.map((o) => [o.field, o])), [orderBy]);
3468
+ const termByField = useMemo4(() => new Map(orderBy.map((o) => [o.field, o])), [orderBy]);
2157
3469
  function previewFields() {
2158
3470
  if (!drag) return termFields;
2159
3471
  const without = termFields.filter((f) => f !== drag.source);
@@ -2183,14 +3495,14 @@ function OrderRow({
2183
3495
  const sortable = fields.filter(isSortable2);
2184
3496
  const isDefaultOrderBy = orderBy.length === api.defaults.orderBy.length && orderBy.every((term, i) => {
2185
3497
  const next = api.defaults.orderBy[i];
2186
- return term.field === next?.field && term.dir === next?.dir && (term.nulls ?? "last") === (next?.nulls ?? "last");
3498
+ return term.field === next?.field && term.dir === next?.dir && (term.nulls ?? "last") === (next?.nulls ?? "last") && (term.extract?.regex ?? null) === (next?.extract?.regex ?? null);
2187
3499
  });
2188
- return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row", children: [
2189
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "order by" }),
2190
- orderBy.length === 0 && !adding && /* @__PURE__ */ jsx5("span", { className: "qt-qb-hint", children: "(default)" }),
3500
+ return /* @__PURE__ */ jsxs6("div", { className: "qt-qb-row", children: [
3501
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-kw", children: "order by" }),
3502
+ orderBy.length === 0 && !adding && /* @__PURE__ */ jsx6("span", { className: "qt-qb-hint", children: "(default)" }),
2191
3503
  rendered.map((f, pos) => {
2192
3504
  const term = termByField.get(f);
2193
- return /* @__PURE__ */ jsx5(
3505
+ return /* @__PURE__ */ jsx6(
2194
3506
  OrderTermChip,
2195
3507
  {
2196
3508
  api,
@@ -2206,15 +3518,26 @@ function OrderRow({
2206
3518
  onDragEnd: () => setDrag(null),
2207
3519
  disabled,
2208
3520
  updateTerm,
3521
+ clearExtract,
2209
3522
  removeTerm,
3523
+ position: pos,
3524
+ count: rendered.length,
3525
+ moveTerm: (toIndex) => {
3526
+ const next = [...orderBy];
3527
+ const fromIndex = next.findIndex((candidate) => candidate.field === f);
3528
+ if (fromIndex === -1) return;
3529
+ const [moved] = next.splice(fromIndex, 1);
3530
+ next.splice(toIndex, 0, moved);
3531
+ setOrderBy(next);
3532
+ },
2210
3533
  classNames,
2211
3534
  label: byName.get(f)?.label ?? f
2212
3535
  },
2213
3536
  f
2214
3537
  );
2215
3538
  }),
2216
- adding ? /* @__PURE__ */ jsx5(FieldPicker, { fields: sortable, onPick: addTerm, onClose: () => setAdding(false) }) : /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-add", onClick: () => setAdding(true), disabled, children: "+ add sort" }),
2217
- !isDefaultOrderBy && /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-link-btn", onClick: () => api.setSort(api.defaults.orderBy), disabled, children: "reset" })
3539
+ adding ? /* @__PURE__ */ jsx6(FieldPicker, { fields: sortable, onPick: addTerm, onClose: () => setAdding(false) }) : /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-add", onClick: () => setAdding(true), disabled, children: "+ add sort" }),
3540
+ !isDefaultOrderBy && /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-link-btn", onClick: () => api.setSort(api.defaults.orderBy), disabled, children: "reset" })
2218
3541
  ] });
2219
3542
  }
2220
3543
  function WindowRow({
@@ -2223,10 +3546,10 @@ function WindowRow({
2223
3546
  disabled
2224
3547
  }) {
2225
3548
  const { query } = api;
2226
- const [limitDraft, setLimitDraft] = useState4(String(query.limit));
2227
- const [offsetDraft, setOffsetDraft] = useState4(String(query.offset));
2228
- useEffect4(() => setLimitDraft(String(query.limit)), [query.limit]);
2229
- useEffect4(() => setOffsetDraft(String(query.offset)), [query.offset]);
3549
+ const [limitDraft, setLimitDraft] = useState5(String(query.limit));
3550
+ const [offsetDraft, setOffsetDraft] = useState5(String(query.offset));
3551
+ useEffect5(() => setLimitDraft(String(query.limit)), [query.limit]);
3552
+ useEffect5(() => setOffsetDraft(String(query.offset)), [query.offset]);
2230
3553
  const hasLimitDefault = query.limit === api.defaults.limit;
2231
3554
  const hasOffsetDefault = query.offset === api.defaults.offset;
2232
3555
  const hasWindowDefault = hasLimitDefault && hasOffsetDefault;
@@ -2236,36 +3559,40 @@ function WindowRow({
2236
3559
  function commitOffset() {
2237
3560
  api.setOffset(Math.max(0, Number(offsetDraft) || 0));
2238
3561
  }
2239
- return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row qt-qb-pagination", children: [
2240
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "limit" }),
2241
- /* @__PURE__ */ jsx5(
2242
- "input",
2243
- {
2244
- className: cx3("qt-qb-num", classNames?.input),
2245
- type: "number",
2246
- min: 1,
2247
- value: limitDraft,
2248
- disabled,
2249
- onChange: (e) => setLimitDraft(e.target.value),
2250
- onBlur: commitLimit,
2251
- onKeyDown: (e) => e.key === "Enter" && commitLimit()
2252
- }
2253
- ),
2254
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "offset" }),
2255
- /* @__PURE__ */ jsx5(
2256
- "input",
2257
- {
2258
- className: cx3("qt-qb-num", classNames?.input),
2259
- type: "number",
2260
- min: 0,
2261
- value: offsetDraft,
2262
- disabled,
2263
- onChange: (e) => setOffsetDraft(e.target.value),
2264
- onBlur: commitOffset,
2265
- onKeyDown: (e) => e.key === "Enter" && commitOffset()
2266
- }
2267
- ),
2268
- !hasWindowDefault && /* @__PURE__ */ jsx5(
3562
+ return /* @__PURE__ */ jsxs6("div", { className: "qt-qb-row qt-qb-pagination", children: [
3563
+ /* @__PURE__ */ jsxs6("label", { className: "qt-qb-window-field", children: [
3564
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-kw", children: "limit" }),
3565
+ /* @__PURE__ */ jsx6(
3566
+ "input",
3567
+ {
3568
+ className: cx3("qt-qb-num", classNames?.input),
3569
+ type: "number",
3570
+ min: 1,
3571
+ value: limitDraft,
3572
+ disabled,
3573
+ onChange: (e) => setLimitDraft(e.target.value),
3574
+ onBlur: commitLimit,
3575
+ onKeyDown: (e) => e.key === "Enter" && commitLimit()
3576
+ }
3577
+ )
3578
+ ] }),
3579
+ /* @__PURE__ */ jsxs6("label", { className: "qt-qb-window-field", children: [
3580
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-kw", children: "offset" }),
3581
+ /* @__PURE__ */ jsx6(
3582
+ "input",
3583
+ {
3584
+ className: cx3("qt-qb-num", classNames?.input),
3585
+ type: "number",
3586
+ min: 0,
3587
+ value: offsetDraft,
3588
+ disabled,
3589
+ onChange: (e) => setOffsetDraft(e.target.value),
3590
+ onBlur: commitOffset,
3591
+ onKeyDown: (e) => e.key === "Enter" && commitOffset()
3592
+ }
3593
+ )
3594
+ ] }),
3595
+ !hasWindowDefault && /* @__PURE__ */ jsx6(
2269
3596
  "button",
2270
3597
  {
2271
3598
  type: "button",
@@ -2298,11 +3625,11 @@ function MetricsRow({
2298
3625
  }) {
2299
3626
  const { aggregations } = api;
2300
3627
  const clauses = aggregations.clauses;
2301
- const measurable = useMemo3(() => fields.filter(isMeasurable), [fields]);
2302
- const groupable = useMemo3(() => fields.filter(isGroupable), [fields]);
2303
- const [drag, setDrag] = useState4(null);
3628
+ const measurable = useMemo4(() => fields.filter(isMeasurable), [fields]);
3629
+ const groupable = useMemo4(() => fields.filter(isGroupable), [fields]);
3630
+ const [drag, setDrag] = useState5(null);
2304
3631
  const ids = clauses.map((c) => c.id);
2305
- const byId = useMemo3(() => new Map(clauses.map((c) => [c.id, c])), [clauses]);
3632
+ const byId = useMemo4(() => new Map(clauses.map((c) => [c.id, c])), [clauses]);
2306
3633
  function previewIds() {
2307
3634
  if (!drag) return ids;
2308
3635
  const without = ids.filter((i) => i !== drag.source);
@@ -2328,12 +3655,12 @@ function MetricsRow({
2328
3655
  setDrag(null);
2329
3656
  }
2330
3657
  const rendered = previewIds();
2331
- return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row qt-qb-row--metrics", children: [
2332
- /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "metrics" }),
2333
- clauses.length === 0 && /* @__PURE__ */ jsx5("span", { className: "qt-qb-hint", children: "none" }),
2334
- rendered.map((id) => {
3658
+ return /* @__PURE__ */ jsxs6("div", { className: "qt-qb-row qt-qb-row--metrics", children: [
3659
+ /* @__PURE__ */ jsx6("span", { className: "qt-qb-kw", children: "metrics" }),
3660
+ clauses.length === 0 && /* @__PURE__ */ jsx6("span", { className: "qt-qb-hint", children: "none" }),
3661
+ rendered.map((id, position) => {
2335
3662
  const clause = byId.get(id);
2336
- return /* @__PURE__ */ jsx5(
3663
+ return /* @__PURE__ */ jsx6(
2337
3664
  MetricChip,
2338
3665
  {
2339
3666
  clause,
@@ -2350,13 +3677,16 @@ function MetricsRow({
2350
3677
  onChangeField: (field) => aggregations.update(id, { field }),
2351
3678
  onAddGroup: (field) => aggregations.update(id, { groupBy: [...clause.groupBy, field] }),
2352
3679
  onRemoveGroup: (field) => aggregations.update(id, { groupBy: clause.groupBy.filter((g) => g !== field) }),
2353
- onRemove: () => aggregations.remove(id)
3680
+ onRemove: () => aggregations.remove(id),
3681
+ position,
3682
+ count: rendered.length,
3683
+ onMove: (toIndex) => aggregations.move(id, toIndex)
2354
3684
  },
2355
3685
  id
2356
3686
  );
2357
3687
  }),
2358
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-add", onClick: () => aggregations.add(), disabled, children: "+ add metric" }),
2359
- clauses.length > 0 && /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-link-btn", onClick: aggregations.clear, disabled, title: "Remove all metrics", children: "reset" })
3688
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-add", onClick: () => aggregations.add(), disabled, children: "+ add metric" }),
3689
+ clauses.length > 0 && /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-link-btn", onClick: aggregations.clear, disabled, title: "Remove all metrics", children: "reset" })
2360
3690
  ] });
2361
3691
  }
2362
3692
  function changeMetricOp(aggregations, clause, measurable, op) {
@@ -2380,13 +3710,16 @@ function MetricChip({
2380
3710
  onChangeField,
2381
3711
  onAddGroup,
2382
3712
  onRemoveGroup,
2383
- onRemove
3713
+ onRemove,
3714
+ position,
3715
+ count,
3716
+ onMove
2384
3717
  }) {
2385
- const [addingGroup, setAddingGroup] = useState4(false);
2386
- const byName = useMemo3(() => new Map(groupable.map((f) => [f.name, f])), [groupable]);
3718
+ const [addingGroup, setAddingGroup] = useState5(false);
3719
+ const byName = useMemo4(() => new Map(groupable.map((f) => [f.name, f])), [groupable]);
2387
3720
  const needsField = aggOpNeedsField(clause.op);
2388
3721
  const measureOptions = measurable.filter((f) => aggOpsForField(f).includes(clause.op));
2389
- return /* @__PURE__ */ jsxs5(
3722
+ return /* @__PURE__ */ jsxs6(
2390
3723
  "span",
2391
3724
  {
2392
3725
  className: cx3("qt-chip", "qt-chip--agg", isDragSource && "qt-chip--dragging", classNames?.chip),
@@ -2404,18 +3737,18 @@ function MetricChip({
2404
3737
  onDragEnd,
2405
3738
  title: "drag to reorder metrics",
2406
3739
  children: [
2407
- /* @__PURE__ */ jsx5("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
2408
- /* @__PURE__ */ jsx5(
3740
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
3741
+ /* @__PURE__ */ jsx6(
2409
3742
  "select",
2410
3743
  {
2411
3744
  className: cx3("qt-chip-op", classNames?.select),
2412
3745
  value: clause.op,
2413
3746
  disabled,
2414
3747
  onChange: (e) => onChangeOp(e.target.value),
2415
- children: AGG_OPS.map((op) => /* @__PURE__ */ jsx5("option", { value: op, children: AGG_OP_LABELS[op] }, op))
3748
+ children: AGG_OPS.map((op) => /* @__PURE__ */ jsx6("option", { value: op, children: AGG_OP_LABELS[op] }, op))
2416
3749
  }
2417
3750
  ),
2418
- /* @__PURE__ */ jsxs5(
3751
+ /* @__PURE__ */ jsxs6(
2419
3752
  "select",
2420
3753
  {
2421
3754
  className: cx3("qt-chip-val", classNames?.select),
@@ -2423,18 +3756,18 @@ function MetricChip({
2423
3756
  disabled,
2424
3757
  onChange: (e) => onChangeField(e.target.value || void 0),
2425
3758
  children: [
2426
- !needsField && /* @__PURE__ */ jsx5("option", { value: "", children: "(all rows)" }),
2427
- needsField && clause.field == null && /* @__PURE__ */ jsx5("option", { value: "", children: "measure\u2026" }),
2428
- measureOptions.map((f) => /* @__PURE__ */ jsx5("option", { value: f.name, children: f.label }, f.name))
3759
+ !needsField && /* @__PURE__ */ jsx6("option", { value: "", children: "(all rows)" }),
3760
+ needsField && clause.field == null && /* @__PURE__ */ jsx6("option", { value: "", children: "measure\u2026" }),
3761
+ measureOptions.map((f) => /* @__PURE__ */ jsx6("option", { value: f.name, children: f.label }, f.name))
2429
3762
  ]
2430
3763
  }
2431
3764
  ),
2432
- /* @__PURE__ */ jsxs5("span", { className: "qt-chip-agg-by", children: [
2433
- clause.groupBy.map((g) => /* @__PURE__ */ jsxs5("span", { className: "qt-chip-agg-group", children: [
2434
- /* @__PURE__ */ jsx5("span", { className: "qt-chip-agg-group-label", children: byName.get(g)?.label ?? g }),
2435
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: () => onRemoveGroup(g), disabled, children: "\u2715" })
3765
+ /* @__PURE__ */ jsxs6("span", { className: "qt-chip-agg-by", children: [
3766
+ clause.groupBy.map((g) => /* @__PURE__ */ jsxs6("span", { className: "qt-chip-agg-group", children: [
3767
+ /* @__PURE__ */ jsx6("span", { className: "qt-chip-agg-group-label", children: byName.get(g)?.label ?? g }),
3768
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-chip-x", onClick: () => onRemoveGroup(g), disabled, children: "\u2715" })
2436
3769
  ] }, g)),
2437
- addingGroup ? /* @__PURE__ */ jsx5(
3770
+ addingGroup ? /* @__PURE__ */ jsx6(
2438
3771
  FieldPicker,
2439
3772
  {
2440
3773
  fields: groupable,
@@ -2445,7 +3778,7 @@ function MetricChip({
2445
3778
  },
2446
3779
  onClose: () => setAddingGroup(false)
2447
3780
  }
2448
- ) : /* @__PURE__ */ jsx5(
3781
+ ) : /* @__PURE__ */ jsx6(
2449
3782
  "button",
2450
3783
  {
2451
3784
  type: "button",
@@ -2457,16 +3790,26 @@ function MetricChip({
2457
3790
  }
2458
3791
  )
2459
3792
  ] }),
2460
- /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: onRemove, disabled, title: "Remove metric", children: "\u2715" })
3793
+ /* @__PURE__ */ jsx6(
3794
+ MoveButtons,
3795
+ {
3796
+ label: clause.label ?? `${AGG_OP_LABELS[clause.op]} metric`,
3797
+ position,
3798
+ count,
3799
+ disabled,
3800
+ onMove
3801
+ }
3802
+ ),
3803
+ /* @__PURE__ */ jsx6("button", { type: "button", className: "qt-chip-x", onClick: onRemove, disabled, title: "Remove metric", children: "\u2715" })
2461
3804
  ]
2462
3805
  }
2463
3806
  );
2464
3807
  }
2465
3808
 
2466
3809
  // src/MetricsPanel.tsx
2467
- import { useMemo as useMemo4 } from "react";
3810
+ import { useMemo as useMemo5 } from "react";
2468
3811
  import { EMPTY_QUERY } from "@pythia-software/query-table-core";
2469
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
3812
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2470
3813
  var cx4 = (...parts) => parts.filter((p) => Boolean(p)).join(" ");
2471
3814
  var OP_LABELS = {
2472
3815
  count: "Count",
@@ -2484,18 +3827,18 @@ function MetricsPanel({
2484
3827
  emptyMessage,
2485
3828
  classNames
2486
3829
  }) {
2487
- const byName = useMemo4(() => new Map(fields.map((f) => [f.name, f])), [fields]);
3830
+ const byName = useMemo5(() => new Map(fields.map((f) => [f.name, f])), [fields]);
2488
3831
  const { clauses, results, loading, error } = aggregations;
2489
3832
  if (clauses.length === 0) {
2490
- return emptyMessage ? /* @__PURE__ */ jsx6("div", { className: cx4("qt-metrics", classNames?.root), children: emptyMessage }) : null;
3833
+ return emptyMessage ? /* @__PURE__ */ jsx7("div", { className: cx4("qt-metrics", classNames?.root), children: emptyMessage }) : null;
2491
3834
  }
2492
3835
  const resultById = new Map((results?.metrics ?? []).map((m) => [m.id, m.buckets]));
2493
- return /* @__PURE__ */ jsxs6("div", { className: cx4("qt-metrics", classNames?.root), children: [
2494
- error ? /* @__PURE__ */ jsxs6("div", { className: "qt-metrics-error", children: [
3836
+ return /* @__PURE__ */ jsxs7("div", { className: cx4("qt-metrics", classNames?.root), children: [
3837
+ error ? /* @__PURE__ */ jsxs7("div", { className: "qt-metrics-error", children: [
2495
3838
  "Metrics failed: ",
2496
3839
  error.message
2497
3840
  ] }) : null,
2498
- clauses.map((clause) => /* @__PURE__ */ jsx6(
3841
+ clauses.map((clause) => /* @__PURE__ */ jsx7(
2499
3842
  MetricCard,
2500
3843
  {
2501
3844
  clause,
@@ -2519,23 +3862,23 @@ function MetricCard({
2519
3862
  }) {
2520
3863
  const measure = clause.field ? byName.get(clause.field) : void 0;
2521
3864
  const groupLabels = clause.groupBy.map((g) => byName.get(g)?.label ?? g);
2522
- const decimals = useMemo4(() => cardDecimals(buckets), [buckets]);
3865
+ const decimals = useMemo5(() => cardDecimals(buckets), [buckets]);
2523
3866
  const fmt = (v) => formatMetricValue(v, clause.op, measure, renderers, decimals);
2524
- return /* @__PURE__ */ jsxs6("div", { className: cx4("qt-metric", `qt-metric--gb${Math.min(clause.groupBy.length, 3)}`, className), children: [
2525
- /* @__PURE__ */ jsxs6("div", { className: "qt-metric-head", children: [
2526
- /* @__PURE__ */ jsx6("span", { className: "qt-metric-title", children: metricTitle(clause, measure) }),
2527
- groupLabels.length > 0 && /* @__PURE__ */ jsxs6("span", { className: "qt-metric-by", children: [
3867
+ return /* @__PURE__ */ jsxs7("div", { className: cx4("qt-metric", `qt-metric--gb${Math.min(clause.groupBy.length, 3)}`, className), children: [
3868
+ /* @__PURE__ */ jsxs7("div", { className: "qt-metric-head", children: [
3869
+ /* @__PURE__ */ jsx7("span", { className: "qt-metric-title", children: metricTitle(clause, measure) }),
3870
+ groupLabels.length > 0 && /* @__PURE__ */ jsxs7("span", { className: "qt-metric-by", children: [
2528
3871
  "by ",
2529
3872
  groupLabels.join(" \xD7 ")
2530
3873
  ] })
2531
3874
  ] }),
2532
- loading && buckets.length === 0 ? /* @__PURE__ */ jsx6("div", { className: "qt-metric-loading", children: "\u2026" }) : buckets.length === 0 ? /* @__PURE__ */ jsx6("div", { className: "qt-metric-empty", children: "no data" }) : clause.groupBy.length === 0 ? /* @__PURE__ */ jsx6(BigNumber, { bucket: buckets[0], fmt }) : clause.groupBy.length === 1 ? /* @__PURE__ */ jsx6(BarList, { buckets, fmt }) : clause.groupBy.length === 2 ? /* @__PURE__ */ jsx6(Pivot, { buckets, fmt, rowLabel: groupLabels[0], colLabel: groupLabels[1] }) : /* @__PURE__ */ jsx6(FlatTable, { buckets, fmt, groupLabels, valueLabel: metricTitle(clause, measure) })
3875
+ loading && buckets.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "qt-metric-loading", children: "\u2026" }) : buckets.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "qt-metric-empty", children: "no data" }) : clause.groupBy.length === 0 ? /* @__PURE__ */ jsx7(BigNumber, { bucket: buckets[0], fmt }) : clause.groupBy.length === 1 ? /* @__PURE__ */ jsx7(BarList, { buckets, fmt }) : clause.groupBy.length === 2 ? /* @__PURE__ */ jsx7(Pivot, { buckets, fmt, rowLabel: groupLabels[0], colLabel: groupLabels[1] }) : /* @__PURE__ */ jsx7(FlatTable, { buckets, fmt, groupLabels, valueLabel: metricTitle(clause, measure) })
2533
3876
  ] });
2534
3877
  }
2535
3878
  function BigNumber({ bucket, fmt }) {
2536
- return /* @__PURE__ */ jsxs6("div", { className: "qt-metric-big", children: [
2537
- /* @__PURE__ */ jsx6("span", { className: "qt-metric-big-value", children: fmt(bucket.value) }),
2538
- /* @__PURE__ */ jsxs6("span", { className: "qt-metric-big-sub", children: [
3879
+ return /* @__PURE__ */ jsxs7("div", { className: "qt-metric-big", children: [
3880
+ /* @__PURE__ */ jsx7("span", { className: "qt-metric-big-value", children: fmt(bucket.value) }),
3881
+ /* @__PURE__ */ jsxs7("span", { className: "qt-metric-big-sub", children: [
2539
3882
  bucket.count.toLocaleString(),
2540
3883
  " rows"
2541
3884
  ] })
@@ -2543,12 +3886,12 @@ function BigNumber({ bucket, fmt }) {
2543
3886
  }
2544
3887
  function BarList({ buckets, fmt }) {
2545
3888
  const max = Math.max(...buckets.map((b) => barMagnitude(b)), 0);
2546
- return /* @__PURE__ */ jsx6("div", { className: "qt-metric-bars", children: buckets.map((b, i) => {
3889
+ return /* @__PURE__ */ jsx7("div", { className: "qt-metric-bars", children: buckets.map((b, i) => {
2547
3890
  const pct = max > 0 ? barMagnitude(b) / max * 100 : 0;
2548
- return /* @__PURE__ */ jsxs6("div", { className: "qt-metric-bar-row", children: [
2549
- /* @__PURE__ */ jsx6("span", { className: "qt-metric-bar-label", title: keyText(b.keys[0]), children: keyNode(b.keys[0]) }),
2550
- /* @__PURE__ */ jsx6("span", { className: "qt-metric-bar-track", children: /* @__PURE__ */ jsx6("span", { className: "qt-metric-bar-fill", style: { width: `${pct}%` } }) }),
2551
- /* @__PURE__ */ jsx6("span", { className: "qt-metric-bar-value", children: fmt(b.value) })
3891
+ return /* @__PURE__ */ jsxs7("div", { className: "qt-metric-bar-row", children: [
3892
+ /* @__PURE__ */ jsx7("span", { className: "qt-metric-bar-label", title: keyText(b.keys[0]), children: keyNode(b.keys[0]) }),
3893
+ /* @__PURE__ */ jsx7("span", { className: "qt-metric-bar-track", children: /* @__PURE__ */ jsx7("span", { className: "qt-metric-bar-fill", style: { width: `${pct}%` } }) }),
3894
+ /* @__PURE__ */ jsx7("span", { className: "qt-metric-bar-value", children: fmt(b.value) })
2552
3895
  ] }, `${keyText(b.keys[0])}-${i}`);
2553
3896
  }) });
2554
3897
  }
@@ -2562,22 +3905,22 @@ function Pivot({
2562
3905
  const colKeys = distinctKeys(buckets, 1);
2563
3906
  const cell = /* @__PURE__ */ new Map();
2564
3907
  for (const b of buckets) cell.set(`${keyText(b.keys[0])}\0${keyText(b.keys[1])}`, b.value);
2565
- return /* @__PURE__ */ jsx6("div", { className: "qt-metric-pivot-wrap", children: /* @__PURE__ */ jsxs6("table", { className: "qt-metric-pivot", children: [
2566
- /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsxs6("tr", { children: [
2567
- /* @__PURE__ */ jsx6("th", { className: "qt-metric-pivot-corner", children: /* @__PURE__ */ jsxs6("span", { className: "qt-metric-pivot-axes", children: [
3908
+ return /* @__PURE__ */ jsx7("div", { className: "qt-metric-pivot-wrap", children: /* @__PURE__ */ jsxs7("table", { className: "qt-metric-pivot", children: [
3909
+ /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsxs7("tr", { children: [
3910
+ /* @__PURE__ */ jsx7("th", { className: "qt-metric-pivot-corner", children: /* @__PURE__ */ jsxs7("span", { className: "qt-metric-pivot-axes", children: [
2568
3911
  rowLabel,
2569
3912
  " ",
2570
- /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: "\\" }),
3913
+ /* @__PURE__ */ jsx7("span", { className: "qt-muted", children: "\\" }),
2571
3914
  " ",
2572
3915
  colLabel
2573
3916
  ] }) }),
2574
- colKeys.map((c) => /* @__PURE__ */ jsx6("th", { className: "qt-metric-pivot-col", children: keyNode(c) }, keyText(c)))
3917
+ colKeys.map((c) => /* @__PURE__ */ jsx7("th", { className: "qt-metric-pivot-col", children: keyNode(c) }, keyText(c)))
2575
3918
  ] }) }),
2576
- /* @__PURE__ */ jsx6("tbody", { children: rowKeys.map((r) => /* @__PURE__ */ jsxs6("tr", { children: [
2577
- /* @__PURE__ */ jsx6("th", { className: "qt-metric-pivot-row", children: keyNode(r) }),
3919
+ /* @__PURE__ */ jsx7("tbody", { children: rowKeys.map((r) => /* @__PURE__ */ jsxs7("tr", { children: [
3920
+ /* @__PURE__ */ jsx7("th", { className: "qt-metric-pivot-row", children: keyNode(r) }),
2578
3921
  colKeys.map((c) => {
2579
3922
  const v = cell.get(`${keyText(r)}\0${keyText(c)}`);
2580
- return /* @__PURE__ */ jsx6("td", { className: "qt-metric-pivot-cell", children: v === void 0 ? /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: "\xB7" }) : fmt(v) }, keyText(c));
3923
+ return /* @__PURE__ */ jsx7("td", { className: "qt-metric-pivot-cell", children: v === void 0 ? /* @__PURE__ */ jsx7("span", { className: "qt-muted", children: "\xB7" }) : fmt(v) }, keyText(c));
2581
3924
  })
2582
3925
  ] }, keyText(r))) })
2583
3926
  ] }) });
@@ -2586,16 +3929,16 @@ function FlatTable({
2586
3929
  buckets,
2587
3930
  fmt,
2588
3931
  groupLabels,
2589
- valueLabel
3932
+ valueLabel: valueLabel2
2590
3933
  }) {
2591
- return /* @__PURE__ */ jsx6("div", { className: "qt-metric-pivot-wrap", children: /* @__PURE__ */ jsxs6("table", { className: "qt-metric-flat", children: [
2592
- /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsxs6("tr", { children: [
2593
- groupLabels.map((g) => /* @__PURE__ */ jsx6("th", { children: g }, g)),
2594
- /* @__PURE__ */ jsx6("th", { className: "qt-metric-flat-value", children: valueLabel })
3934
+ return /* @__PURE__ */ jsx7("div", { className: "qt-metric-pivot-wrap", children: /* @__PURE__ */ jsxs7("table", { className: "qt-metric-flat", children: [
3935
+ /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsxs7("tr", { children: [
3936
+ groupLabels.map((g) => /* @__PURE__ */ jsx7("th", { children: g }, g)),
3937
+ /* @__PURE__ */ jsx7("th", { className: "qt-metric-flat-value", children: valueLabel2 })
2595
3938
  ] }) }),
2596
- /* @__PURE__ */ jsx6("tbody", { children: buckets.map((b, i) => /* @__PURE__ */ jsxs6("tr", { children: [
2597
- groupLabels.map((_, j) => /* @__PURE__ */ jsx6("td", { children: keyNode(b.keys[j]) }, j)),
2598
- /* @__PURE__ */ jsx6("td", { className: "qt-metric-flat-value", children: fmt(b.value) })
3939
+ /* @__PURE__ */ jsx7("tbody", { children: buckets.map((b, i) => /* @__PURE__ */ jsxs7("tr", { children: [
3940
+ groupLabels.map((_, j) => /* @__PURE__ */ jsx7("td", { children: keyNode(b.keys[j]) }, j)),
3941
+ /* @__PURE__ */ jsx7("td", { className: "qt-metric-flat-value", children: fmt(b.value) })
2599
3942
  ] }, i)) })
2600
3943
  ] }) });
2601
3944
  }
@@ -2606,7 +3949,7 @@ function metricTitle(clause, measure) {
2606
3949
  }
2607
3950
  var UNIT_RENDER_KEYS = /* @__PURE__ */ new Set(["duration_ms", "byte_size"]);
2608
3951
  function formatMetricValue(value, op, measure, renderers, decimals) {
2609
- if (value == null) return /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: "\u2014" });
3952
+ if (value == null) return /* @__PURE__ */ jsx7("span", { className: "qt-muted", children: "\u2014" });
2610
3953
  if (op === "count" || op === "count_distinct") {
2611
3954
  return typeof value === "number" ? formatNumber(value, 0) : String(value);
2612
3955
  }
@@ -2669,21 +4012,21 @@ function keyText(k) {
2669
4012
  return k == null ? NULL_KEY : String(k);
2670
4013
  }
2671
4014
  function keyNode(k) {
2672
- return k == null ? /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: NULL_KEY }) : String(k);
4015
+ return k == null ? /* @__PURE__ */ jsx7("span", { className: "qt-muted", children: NULL_KEY }) : String(k);
2673
4016
  }
2674
4017
 
2675
4018
  // src/SelectionToolbar.tsx
2676
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
4019
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
2677
4020
  var cx5 = (...parts) => parts.filter(Boolean).join(" ");
2678
4021
  function SelectionToolbar({ selection, actions, alwaysShow, className }) {
2679
4022
  if (selection.count === 0 && !alwaysShow) return null;
2680
4023
  const selectedIds = [...selection.selected];
2681
- return /* @__PURE__ */ jsxs7("div", { className: cx5("qt-selection-toolbar", className), role: "toolbar", children: [
2682
- /* @__PURE__ */ jsxs7("span", { className: "qt-selection-count", children: [
4024
+ return /* @__PURE__ */ jsxs8("div", { className: cx5("qt-selection-toolbar", className), role: "toolbar", children: [
4025
+ /* @__PURE__ */ jsxs8("span", { className: "qt-selection-count", children: [
2683
4026
  selection.count,
2684
4027
  " selected"
2685
4028
  ] }),
2686
- /* @__PURE__ */ jsx7(
4029
+ /* @__PURE__ */ jsx8(
2687
4030
  "button",
2688
4031
  {
2689
4032
  type: "button",
@@ -2693,7 +4036,7 @@ function SelectionToolbar({ selection, actions, alwaysShow, className }) {
2693
4036
  children: "clear"
2694
4037
  }
2695
4038
  ),
2696
- /* @__PURE__ */ jsx7("span", { className: "qt-selection-actions", children: actions(selectedIds) })
4039
+ /* @__PURE__ */ jsx8("span", { className: "qt-selection-actions", children: actions(selectedIds, selection) })
2697
4040
  ] });
2698
4041
  }
2699
4042
  export {
@@ -2703,6 +4046,7 @@ export {
2703
4046
  MetricsPanel,
2704
4047
  QueryBuilder,
2705
4048
  SavedQueriesModal,
4049
+ SelectColumnEditor,
2706
4050
  SelectionToolbar,
2707
4051
  defaultRenderers,
2708
4052
  resolveRenderer,