epiq 1.4.2 → 1.4.4

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/gui/main.js CHANGED
@@ -24858,7 +24858,14 @@ var GUI_THEME = {
24858
24858
  dim2: "rgb(100 107 133)",
24859
24859
  accent: "#76d4ff",
24860
24860
  green: "#8ce99a",
24861
- red: "#ff8787"
24861
+ red: "#ff8787",
24862
+ transparent: "rgba(0, 0, 0, 0)"
24863
+ };
24864
+ var EVENT_CATEGORY_COLORS = {
24865
+ tickets: GUI_THEME.accent,
24866
+ comments: "#ffd479",
24867
+ tagging: "#c9a5ff",
24868
+ assigning: "#ff9ecd"
24862
24869
  };
24863
24870
 
24864
24871
  // source/gui/client/components/Aside.tsx
@@ -25318,11 +25325,16 @@ var User = ({
25318
25325
  );
25319
25326
 
25320
25327
  // source/version.ts
25321
- var EPIQ_VERSION = "1.4.2";
25328
+ var EPIQ_VERSION = "1.4.4";
25322
25329
 
25323
25330
  // source/gui/client/components/Header.tsx
25324
25331
  var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
25325
- var Header = ({ state, connected, syncStatus }) => {
25332
+ var Header = ({
25333
+ state,
25334
+ connected,
25335
+ scrubbing,
25336
+ syncStatus
25337
+ }) => {
25326
25338
  const syncColor = syncStatus.status === "synced" ? GUI_THEME.green : syncStatus.status === "failed" ? GUI_THEME.red : GUI_THEME.accent;
25327
25339
  const syncLabel = syncStatus.status === "failed" ? "-" : syncStatus.msg.toLowerCase();
25328
25340
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
@@ -25381,6 +25393,10 @@ var Header = ({ state, connected, syncStatus }) => {
25381
25393
  whiteSpace: "nowrap"
25382
25394
  },
25383
25395
  children: [
25396
+ scrubbing && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
25397
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { color: GUI_THEME.accent, fontWeight: 700 }, children: "Read-only" }),
25398
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { color: GUI_THEME.dim }, children: "|" })
25399
+ ] }),
25384
25400
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
25385
25401
  "span",
25386
25402
  {
@@ -25397,7 +25413,6 @@ var Header = ({ state, connected, syncStatus }) => {
25397
25413
  title: syncStatus.status === "failed" ? syncStatus.msg : void 0,
25398
25414
  style: {
25399
25415
  color: GUI_THEME.dim,
25400
- minWidth: 72,
25401
25416
  maxWidth: 220,
25402
25417
  overflow: "hidden",
25403
25418
  textOverflow: "ellipsis",
@@ -27715,11 +27730,11 @@ function addChildren(props, children) {
27715
27730
  }
27716
27731
  }
27717
27732
  }
27718
- function productionCreate(_, jsx35, jsxs23) {
27733
+ function productionCreate(_, jsx37, jsxs25) {
27719
27734
  return create2;
27720
27735
  function create2(_2, type, props, key) {
27721
27736
  const isStaticChildren = Array.isArray(props.children);
27722
- const fn = isStaticChildren ? jsxs23 : jsx35;
27737
+ const fn = isStaticChildren ? jsxs25 : jsx37;
27723
27738
  return key ? fn(type, props, key) : fn(type, props);
27724
27739
  }
27725
27740
  }
@@ -39647,23 +39662,23 @@ var import_react11 = __toESM(require_react(), 1);
39647
39662
 
39648
39663
  // source/gui/client/components/IconComment.tsx
39649
39664
  var import_jsx_runtime22 = __toESM(require_jsx_runtime(), 1);
39650
- var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39665
+ var IconComment = ({ size = 14 }) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39651
39666
  "svg",
39652
39667
  {
39653
39668
  width: size,
39654
39669
  height: size,
39655
- viewBox: "0 0 16 16",
39670
+ viewBox: "0 0 24 24",
39656
39671
  fill: "none",
39672
+ stroke: "currentColor",
39673
+ strokeWidth: "2",
39674
+ strokeLinecap: "round",
39675
+ strokeLinejoin: "round",
39657
39676
  "aria-hidden": "true",
39658
39677
  children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39659
39678
  "path",
39660
39679
  {
39661
- d: "M3.9 2.6h5.2a2.4 2.4 0 0 1 2.4 2.4v2.5a2.4 2.4 0 0 1-2.4 2.4H6.6l-1.8 2.1.2-2.1H3.9a2.4 2.4 0 0 1-2.4-2.4V5a2.4 2.4 0 0 1 2.4-2.4Z",
39662
39680
  fill: "currentColor",
39663
- stroke: "currentColor",
39664
- strokeWidth: "1.55",
39665
- strokeLinecap: "round",
39666
- strokeLinejoin: "round"
39681
+ d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
39667
39682
  }
39668
39683
  )
39669
39684
  }
@@ -39858,7 +39873,7 @@ var TicketCard = ({
39858
39873
  color: isSelected ? GUI_THEME.accent : GUI_THEME.secondary,
39859
39874
  background: isSelected ? "rgba(118,228,255,0.10)" : "rgba(255,255,255,0.035)",
39860
39875
  border: `1px solid ${isSelected ? "rgba(118,228,255,0.28)" : GUI_THEME.line}`,
39861
- borderRadius: 999,
39876
+ borderRadius: 6,
39862
39877
  padding: "3px 7px",
39863
39878
  fontSize: 11,
39864
39879
  fontWeight: 600,
@@ -39897,7 +39912,15 @@ var TicketCard = ({
39897
39912
  paddingTop: 2,
39898
39913
  marginTop: "-4px"
39899
39914
  },
39900
- children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(User, { user: assignee, index: idx, isFocus: isSelected }))
39915
+ children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39916
+ User,
39917
+ {
39918
+ user: assignee,
39919
+ index: idx,
39920
+ isFocus: isSelected
39921
+ },
39922
+ assignee.id
39923
+ ))
39901
39924
  }
39902
39925
  )
39903
39926
  ]
@@ -40203,7 +40226,7 @@ var EVENTS_MODE_TOTAL_HEIGHT = 8 + TRACK_HEIGHT * 2;
40203
40226
  var EVENTS_SCATTER_HEIGHT = TRACK_HEIGHT + 16;
40204
40227
  var EVENTS_MODE_VERTICAL_PADDING = (EVENTS_MODE_TOTAL_HEIGHT - EVENTS_SCATTER_HEIGHT) / 2;
40205
40228
  var HOVER_HINT_WIDTH = 220;
40206
- var SEGMENT_HIGHLIGHT_COLOR = "rgba(122, 158, 214, 0.07)";
40229
+ var SEGMENT_HIGHLIGHT_COLOR = "rgba(122, 157, 214, 0.14)";
40207
40230
  var BUCKET_HIGHLIGHT_COLOR = "rgba(255, 255, 255, 0.06)";
40208
40231
  var NEEDLE_COLOR = "rgba(255, 255, 255, 0.62)";
40209
40232
  var MIN_BUCKET_COUNT_FOR_GAP = 300;
@@ -40239,8 +40262,15 @@ var buildAxis = (timeline, commits, now = Date.now()) => {
40239
40262
  fractionToTime: (fraction) => Math.round(earliest + clamp(fraction, 0, 1) * span)
40240
40263
  };
40241
40264
  };
40242
- var bucketIssueCounts = (axis, timeline) => {
40265
+ var bucketIssueCounts = (axis, timeline, view = "all", hiddenIds = /* @__PURE__ */ new Set()) => {
40243
40266
  const counts = new Array(axis.bucketCount).fill(0);
40267
+ if (timeline && timeline.events.length > 0) {
40268
+ for (const entry of timeline.events) {
40269
+ if (!isShown(entry, view, hiddenIds)) continue;
40270
+ counts[axis.bucketIndexForTime(entry.t)] += 1;
40271
+ }
40272
+ return counts;
40273
+ }
40244
40274
  for (const sparseBucket of timeline?.buckets ?? []) {
40245
40275
  counts[axis.bucketIndexForTime(sparseBucket.t)] += sparseBucket.count;
40246
40276
  }
@@ -40258,6 +40288,102 @@ var bucketCommitStats = (axis, commits) => {
40258
40288
  }
40259
40289
  return byIndex;
40260
40290
  };
40291
+ var EVENT_CATEGORIES = [
40292
+ "tickets",
40293
+ "comments",
40294
+ "tagging",
40295
+ "assigning"
40296
+ ];
40297
+ var BOARD_VIEWS = ["all", ...EVENT_CATEGORIES];
40298
+ var isBoardView = (value) => BOARD_VIEWS.includes(value);
40299
+ var boardViewColor = (view) => view === "all" ? GUI_THEME.accent : EVENT_CATEGORY_COLORS[view];
40300
+ var identityAxisFor = (view) => view === "comments" ? "actor" : view === "tagging" ? "tag" : view === "assigning" ? "assignee" : null;
40301
+ var CATEGORY_BY_ACTION = {
40302
+ "add.issue.comment": "comments",
40303
+ "edit.issue.comment": "comments",
40304
+ "delete.issue.comment": "comments",
40305
+ "add.issue.tag": "tagging",
40306
+ "remove.issue.tag": "tagging",
40307
+ "create.tag": "tagging",
40308
+ "add.issue.assignee": "assigning",
40309
+ "remove.issue.assignee": "assigning",
40310
+ "create.contributor": "assigning",
40311
+ "tombstone.contributor": "assigning",
40312
+ "restore.contributor": "assigning",
40313
+ "link.contributor.user": "assigning"
40314
+ };
40315
+ var categoryOf = (action) => CATEGORY_BY_ACTION[action] ?? "tickets";
40316
+ var identityOf = (entry, view) => {
40317
+ const axis = identityAxisFor(view);
40318
+ return axis === null ? null : entry[axis];
40319
+ };
40320
+ var listIdentities = (timeline, view) => {
40321
+ if (!timeline || identityAxisFor(view) === null) return [];
40322
+ const byId = /* @__PURE__ */ new Map();
40323
+ for (const entry of timeline.events) {
40324
+ if (categoryOf(entry.action) !== view) continue;
40325
+ const identity = identityOf(entry, view);
40326
+ if (identity && !byId.has(identity.id)) byId.set(identity.id, identity);
40327
+ }
40328
+ return [...byId.values()];
40329
+ };
40330
+ var soleVisibleIdentity = (identities, hiddenIds) => {
40331
+ const visible = identities.filter((identity) => !hiddenIds.has(identity.id));
40332
+ return visible.length === 1 ? visible[0] : null;
40333
+ };
40334
+ var dotDetail = (dot) => {
40335
+ const base = dot.label ?? `${dot.count ?? 0} board event${dot.count === 1 ? "" : "s"}`;
40336
+ return dot.identity && !base.includes(dot.identity.name) ? `${base} \u2014 ${dot.identity.name}` : base;
40337
+ };
40338
+ var EVENT_DOT_SIZE = 4;
40339
+ var EVENT_DOT_OPACITY = 0.55;
40340
+ var isShown = (entry, view, hiddenIds) => {
40341
+ if (view !== "all" && categoryOf(entry.action) !== view) return false;
40342
+ const identity = identityOf(entry, view);
40343
+ return identity === null || !hiddenIds.has(identity.id);
40344
+ };
40345
+ var buildEventDots = (timeline, view = "all", hiddenIds = /* @__PURE__ */ new Set()) => {
40346
+ if (!timeline) return [];
40347
+ if (timeline.events.length > 0) {
40348
+ return timeline.events.flatMap((entry, index2) => {
40349
+ if (!isShown(entry, view, hiddenIds)) return [];
40350
+ const category = categoryOf(entry.action);
40351
+ const identity = identityOf(entry, view);
40352
+ return [
40353
+ {
40354
+ // Two events can share a millisecond, so time alone is not a key.
40355
+ key: `${entry.t}-${index2}`,
40356
+ t: entry.t,
40357
+ count: null,
40358
+ label: entry.label,
40359
+ category,
40360
+ color: view === "all" ? EVENT_CATEGORY_COLORS[category] : identity?.color ?? GUI_THEME.accent,
40361
+ identity,
40362
+ size: EVENT_DOT_SIZE,
40363
+ opacity: EVENT_DOT_OPACITY
40364
+ }
40365
+ ];
40366
+ });
40367
+ }
40368
+ const maxCount = maxOf(
40369
+ timeline.buckets.map((bucket) => bucket.count),
40370
+ 1
40371
+ );
40372
+ return timeline.buckets.map((bucket) => {
40373
+ const intensity = bucket.count / maxCount;
40374
+ return {
40375
+ key: String(bucket.t),
40376
+ t: bucket.t,
40377
+ count: bucket.count,
40378
+ label: null,
40379
+ category: null,
40380
+ color: GUI_THEME.accent,
40381
+ identity: null,
40382
+ size: 3 + intensity * 6,
40383
+ opacity: 0.3 + intensity * 0.5
40384
+ };
40385
+ });
40386
+ };
40261
40387
  var populatedRange = (bars) => bars.length > 0 ? [
40262
40388
  bars.reduce((min, bar) => Math.min(min, bar.index), Infinity),
40263
40389
  bars.reduce((max, bar) => Math.max(max, bar.index), -Infinity)
@@ -40266,8 +40392,9 @@ var hourFractionForTime = (time) => {
40266
40392
  const date = new Date(time);
40267
40393
  return (date.getHours() * 60 + date.getMinutes()) / (24 * 60);
40268
40394
  };
40269
- var SEGMENT_UNIT_ORDER = ["day", "week", "month", "year"];
40395
+ var SEGMENT_UNIT_ORDER = ["hour", "day", "week", "month", "year"];
40270
40396
  var APPROX_UNIT_MS = {
40397
+ hour: 60 * 60 * 1e3,
40271
40398
  day: 24 * 60 * 60 * 1e3,
40272
40399
  week: 7 * 24 * 60 * 60 * 1e3,
40273
40400
  month: 30.44 * 24 * 60 * 60 * 1e3,
@@ -40293,14 +40420,16 @@ var MONTH_LABELS = [
40293
40420
  "Dec"
40294
40421
  ];
40295
40422
  var advanceByUnit = (date, unit) => {
40296
- if (unit === "day") date.setDate(date.getDate() + 1);
40423
+ if (unit === "hour") date.setHours(date.getHours() + 1);
40424
+ else if (unit === "day") date.setDate(date.getDate() + 1);
40297
40425
  else if (unit === "week") date.setDate(date.getDate() + 7);
40298
40426
  else if (unit === "month") date.setMonth(date.getMonth() + 1);
40299
40427
  else date.setFullYear(date.getFullYear() + 1);
40300
40428
  };
40301
40429
  var segmentAt = (time, unit) => {
40302
40430
  const start2 = new Date(time);
40303
- start2.setHours(0, 0, 0, 0);
40431
+ if (unit === "hour") start2.setMinutes(0, 0, 0);
40432
+ else start2.setHours(0, 0, 0, 0);
40304
40433
  if (unit === "week") {
40305
40434
  start2.setDate(start2.getDate() - (start2.getDay() + 6) % 7);
40306
40435
  } else if (unit === "month") {
@@ -40311,7 +40440,10 @@ var segmentAt = (time, unit) => {
40311
40440
  const end = new Date(start2);
40312
40441
  advanceByUnit(end, unit);
40313
40442
  const lastDay = new Date(end.getTime() - 1);
40314
- const label = unit === "day" ? `${WEEKDAY_LABELS[start2.getDay()]} ${start2.getDate()} ${MONTH_LABELS[start2.getMonth()]}` : unit === "week" ? start2.getMonth() === lastDay.getMonth() ? `${start2.getDate()}\u2013${lastDay.getDate()} ${MONTH_LABELS[start2.getMonth()]}` : `${start2.getDate()} ${MONTH_LABELS[start2.getMonth()]} \u2013 ${lastDay.getDate()} ${MONTH_LABELS[lastDay.getMonth()]}` : unit === "month" ? `${MONTH_LABELS[start2.getMonth()]} ${start2.getFullYear()}` : `${start2.getFullYear()}`;
40443
+ const label = unit === "hour" ? `${WEEKDAY_LABELS[start2.getDay()]} ${String(start2.getHours()).padStart(
40444
+ 2,
40445
+ "0"
40446
+ )}:00` : unit === "day" ? `${WEEKDAY_LABELS[start2.getDay()]} ${start2.getDate()} ${MONTH_LABELS[start2.getMonth()]}` : unit === "week" ? start2.getMonth() === lastDay.getMonth() ? `${start2.getDate()}\u2013${lastDay.getDate()} ${MONTH_LABELS[start2.getMonth()]}` : `${start2.getDate()} ${MONTH_LABELS[start2.getMonth()]} \u2013 ${lastDay.getDate()} ${MONTH_LABELS[lastDay.getMonth()]}` : unit === "month" ? `${MONTH_LABELS[start2.getMonth()]} ${start2.getFullYear()}` : `${start2.getFullYear()}`;
40315
40447
  return { start: start2.getTime(), end: end.getTime(), label };
40316
40448
  };
40317
40449
  var formatInterval = (start2, end) => {
@@ -40320,12 +40452,19 @@ var formatInterval = (start2, end) => {
40320
40452
  const endLabel = isSameDay(startDate, endDate) ? formatTimeOfDay(endDate) : formatDateTime(endDate);
40321
40453
  return `${formatDateTime(startDate)} \u2013 ${endLabel}`;
40322
40454
  };
40323
- var SCOPES = ["week", "month", "year", "all"];
40455
+ var SCOPES = ["day", "week", "month", "year", "all"];
40324
40456
  var SCOPE_DURATION_MS = {
40457
+ day: 24 * 60 * 60 * 1e3,
40325
40458
  week: 7 * 24 * 60 * 60 * 1e3,
40326
40459
  month: 30 * 24 * 60 * 60 * 1e3,
40327
40460
  year: 365 * 24 * 60 * 60 * 1e3
40328
40461
  };
40462
+ var SCOPE_RECENT_LABELS = {
40463
+ day: "Last 24 hours",
40464
+ week: "Last 7 days",
40465
+ month: "Last 30 days",
40466
+ year: "Last 365 days"
40467
+ };
40329
40468
  var isScope = (value) => SCOPES.includes(value ?? "");
40330
40469
  var getPeriodRange = (scope, offset) => {
40331
40470
  if (scope === "all") return null;
@@ -40335,9 +40474,7 @@ var getPeriodRange = (scope, offset) => {
40335
40474
  };
40336
40475
  var formatPeriodLabel = (scope, offset, range) => {
40337
40476
  if (scope === "all" || !range) return "All time";
40338
- if (offset === 0) {
40339
- return scope === "week" ? "Last 7 days" : scope === "month" ? "Last 30 days" : "Last 365 days";
40340
- }
40477
+ if (offset === 0) return SCOPE_RECENT_LABELS[scope];
40341
40478
  const start2 = new Date(range.start);
40342
40479
  const end = new Date(range.end);
40343
40480
  return `${start2.getMonth() + 1}/${start2.getDate()} \u2013 ${end.getMonth() + 1}/${end.getDate()}`;
@@ -40354,10 +40491,16 @@ var hashUnitInterval = (key) => {
40354
40491
  var DOT_APPEAR_MS = 260;
40355
40492
  var DOT_APPEAR_SCATTER_MS = 620;
40356
40493
  var dotDelayMs = (key) => Math.round(hashUnitInterval(key) * DOT_APPEAR_SCATTER_MS);
40357
- var dotAppearAnimation = (key) => `epiqScrubberTwinkle ${DOT_APPEAR_MS}ms ease-out ${dotDelayMs(
40358
- key
40359
- )}ms backwards`;
40360
- var dotExitAnimation = (key) => `epiqScrubberRetract ${DOT_APPEAR_MS}ms ease-in ${DOT_APPEAR_SCATTER_MS - dotDelayMs(key)}ms both`;
40494
+ var easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
40495
+ var easeInCubic = (t) => t * t * t;
40496
+ var dotEntranceScale = (key, elapsedMs) => easeOutCubic(clamp((elapsedMs - dotDelayMs(key)) / DOT_APPEAR_MS, 0, 1));
40497
+ var dotExitScale = (key, elapsedMs) => 1 - easeInCubic(
40498
+ clamp(
40499
+ (elapsedMs - (DOT_APPEAR_SCATTER_MS - dotDelayMs(key))) / DOT_APPEAR_MS,
40500
+ 0,
40501
+ 1
40502
+ )
40503
+ );
40361
40504
  var DOT_EXIT_TOTAL_MS = DOT_APPEAR_SCATTER_MS + DOT_APPEAR_MS;
40362
40505
  var BAR_GROW_MS = 200;
40363
40506
  var BAR_GROW_SWEEP_MS = 560;
@@ -40450,6 +40593,19 @@ var usePersistedFlag = (key, fallback) => {
40450
40593
  }
40451
40594
  ];
40452
40595
  };
40596
+ var buildBoardFilter = (view, identities, hiddenIds) => {
40597
+ const axis = identityAxisFor(view);
40598
+ if (axis === null || hiddenIds.size === 0) return null;
40599
+ const visibleIds = new Set(
40600
+ identities.map((identity) => identity.id).filter((id) => !hiddenIds.has(id))
40601
+ );
40602
+ return { axis, visibleIds };
40603
+ };
40604
+ var issuePassesBoardFilter = (issue, commentAuthorIds, filter) => {
40605
+ if (!filter) return true;
40606
+ const ids = filter.axis === "tag" ? issue.tags.map((tag) => tag.id) : filter.axis === "assignee" ? issue.assignees.map((assignee) => assignee.id) : commentAuthorIds;
40607
+ return ids.some((id) => filter.visibleIds.has(id));
40608
+ };
40453
40609
 
40454
40610
  // source/gui/client/components/ScrubberParts.tsx
40455
40611
  var import_react14 = __toESM(require_react(), 1);
@@ -40534,9 +40690,32 @@ var Checkbox = ({
40534
40690
  );
40535
40691
  };
40536
40692
 
40537
- // source/gui/client/components/IconChevronDown.tsx
40693
+ // source/gui/client/components/IconBars.tsx
40538
40694
  var import_jsx_runtime28 = __toESM(require_jsx_runtime(), 1);
40539
- var IconChevronDown = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
40695
+ var IconBars = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
40696
+ "svg",
40697
+ {
40698
+ width: size,
40699
+ height: size,
40700
+ viewBox: "0 0 24 24",
40701
+ fill: "none",
40702
+ stroke: "currentColor",
40703
+ strokeWidth: "2.2",
40704
+ strokeLinecap: "round",
40705
+ "aria-hidden": "true",
40706
+ children: [
40707
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("line", { x1: "3", y1: "8.5", x2: "3", y2: "15.5" }),
40708
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("line", { x1: "7.5", y1: "4.5", x2: "7.5", y2: "19.5" }),
40709
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("line", { x1: "12", y1: "7", x2: "12", y2: "17" }),
40710
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("line", { x1: "16.5", y1: "3", x2: "16.5", y2: "21" }),
40711
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("line", { x1: "21", y1: "9.5", x2: "21", y2: "14.5" })
40712
+ ]
40713
+ }
40714
+ );
40715
+
40716
+ // source/gui/client/components/IconChevronDown.tsx
40717
+ var import_jsx_runtime29 = __toESM(require_jsx_runtime(), 1);
40718
+ var IconChevronDown = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40540
40719
  "svg",
40541
40720
  {
40542
40721
  width: size,
@@ -40548,13 +40727,13 @@ var IconChevronDown = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime2
40548
40727
  strokeLinecap: "round",
40549
40728
  strokeLinejoin: "round",
40550
40729
  "aria-hidden": "true",
40551
- children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("polyline", { points: "6 9 12 15 18 9" })
40730
+ children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("polyline", { points: "6 9 12 15 18 9" })
40552
40731
  }
40553
40732
  );
40554
40733
 
40555
40734
  // source/gui/client/components/IconChevronRight.tsx
40556
- var import_jsx_runtime29 = __toESM(require_jsx_runtime(), 1);
40557
- var IconChevronRight = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40735
+ var import_jsx_runtime30 = __toESM(require_jsx_runtime(), 1);
40736
+ var IconChevronRight = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40558
40737
  "svg",
40559
40738
  {
40560
40739
  width: size,
@@ -40566,12 +40745,33 @@ var IconChevronRight = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime
40566
40745
  strokeLinecap: "round",
40567
40746
  strokeLinejoin: "round",
40568
40747
  "aria-hidden": "true",
40569
- children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("polyline", { points: "9 18 15 12 9 6" })
40748
+ children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("polyline", { points: "9 18 15 12 9 6" })
40749
+ }
40750
+ );
40751
+
40752
+ // source/gui/client/components/IconScatter.tsx
40753
+ var import_jsx_runtime31 = __toESM(require_jsx_runtime(), 1);
40754
+ var IconScatter = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
40755
+ "svg",
40756
+ {
40757
+ width: size,
40758
+ height: size,
40759
+ viewBox: "0 0 24 24",
40760
+ fill: "currentColor",
40761
+ stroke: "none",
40762
+ "aria-hidden": "true",
40763
+ children: [
40764
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("circle", { cx: "4.5", cy: "8", r: "2.2" }),
40765
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("circle", { cx: "9.5", cy: "17", r: "2.2" }),
40766
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("circle", { cx: "13", cy: "6.5", r: "2.2" }),
40767
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("circle", { cx: "17", cy: "14.5", r: "2.2" }),
40768
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("circle", { cx: "20.5", cy: "8.5", r: "2.2" })
40769
+ ]
40570
40770
  }
40571
40771
  );
40572
40772
 
40573
40773
  // source/gui/client/components/ScrubberParts.tsx
40574
- var import_jsx_runtime30 = __toESM(require_jsx_runtime(), 1);
40774
+ var import_jsx_runtime32 = __toESM(require_jsx_runtime(), 1);
40575
40775
  var toggleButtonStyle = (active) => ({
40576
40776
  background: "transparent",
40577
40777
  border: `1px solid ${active ? GUI_THEME.accent : GUI_THEME.dim}`,
@@ -40581,6 +40781,330 @@ var toggleButtonStyle = (active) => ({
40581
40781
  padding: "2px 8px",
40582
40782
  cursor: "pointer"
40583
40783
  });
40784
+ var iconToggleButtonStyle = (active) => ({
40785
+ ...toggleButtonStyle(active),
40786
+ display: "inline-flex",
40787
+ alignItems: "center",
40788
+ justifyContent: "center",
40789
+ padding: "3px 7px"
40790
+ });
40791
+ var CATEGORY_LABELS = {
40792
+ tickets: "Tickets",
40793
+ comments: "Comments",
40794
+ tagging: "Tags",
40795
+ assigning: "Assignees"
40796
+ };
40797
+ var disclosureStyle = {
40798
+ background: "transparent",
40799
+ border: "none",
40800
+ padding: 0,
40801
+ display: "inline-flex",
40802
+ alignItems: "center",
40803
+ color: GUI_THEME.secondary,
40804
+ cursor: "pointer"
40805
+ };
40806
+ var VIEW_LABELS = {
40807
+ all: "All board events",
40808
+ ...CATEGORY_LABELS
40809
+ };
40810
+ var SELECT_TRIGGER_WIDTH = 180;
40811
+ var selectTriggerStyle = (color2, disabled) => ({
40812
+ display: "inline-flex",
40813
+ alignItems: "center",
40814
+ justifyContent: "space-between",
40815
+ gap: 6,
40816
+ width: SELECT_TRIGGER_WIDTH,
40817
+ boxSizing: "border-box",
40818
+ background: "rgba(208, 223, 255, 0.08)",
40819
+ // None, but padded as though there were, so it sits at the same height as
40820
+ // the bordered toggles on either side.
40821
+ border: "none",
40822
+ color: disabled ? GUI_THEME.dim : color2,
40823
+ borderRadius: 6,
40824
+ fontFamily: "inherit",
40825
+ fontSize: 10,
40826
+ padding: "3px 7px 3px 9px",
40827
+ cursor: disabled ? "not-allowed" : "pointer",
40828
+ opacity: disabled ? 0.4 : 1
40829
+ });
40830
+ var nestedListStyle = {
40831
+ display: "flex",
40832
+ flexDirection: "column",
40833
+ gap: 7,
40834
+ marginLeft: 5,
40835
+ paddingLeft: 8,
40836
+ borderLeft: `1px solid ${GUI_THEME.line}`
40837
+ };
40838
+ var popoverStyle = {
40839
+ position: "absolute",
40840
+ top: "100%",
40841
+ left: 0,
40842
+ marginTop: 6,
40843
+ // Carries the column and its gap itself. Without them the options stack as
40844
+ // plain blocks and their radios sit edge to edge.
40845
+ display: "flex",
40846
+ flexDirection: "column",
40847
+ gap: 7,
40848
+ padding: "10px 14px 10px 10px",
40849
+ // Sized for the common case up front, so opening a kind with a list does
40850
+ // not visibly widen the panel under the pointer.
40851
+ minWidth: 200,
40852
+ // Slightly sheer over a blur, so the chart it filters stays legible beneath
40853
+ // it rather than being covered outright.
40854
+ background: "rgba(21, 26, 36, 0.88)",
40855
+ backdropFilter: "blur(12px)",
40856
+ WebkitBackdropFilter: "blur(12px)",
40857
+ border: `1px solid ${GUI_THEME.line}`,
40858
+ borderRadius: 8,
40859
+ boxShadow: "0 8px 24px rgba(0, 0, 0, 0.45)",
40860
+ // Above the dots and the needle, which sit at 1 and 2.
40861
+ zIndex: 30,
40862
+ whiteSpace: "nowrap"
40863
+ };
40864
+ var onlyButtonStyle = {
40865
+ background: "transparent",
40866
+ border: `1px solid ${GUI_THEME.line}`,
40867
+ borderRadius: 4,
40868
+ color: GUI_THEME.dim,
40869
+ fontFamily: "inherit",
40870
+ fontSize: 9,
40871
+ lineHeight: 1,
40872
+ padding: "2px 4px",
40873
+ cursor: "pointer"
40874
+ };
40875
+ var useDismissOnOutsideClick = (open, onDismiss) => {
40876
+ const ref = (0, import_react14.useRef)(null);
40877
+ (0, import_react14.useEffect)(() => {
40878
+ if (!open) return;
40879
+ const onPointerDown = (event) => {
40880
+ if (!ref.current?.contains(event.target)) onDismiss();
40881
+ };
40882
+ const onKeyDown = (event) => {
40883
+ if (event.key === "Escape") onDismiss();
40884
+ };
40885
+ document.addEventListener("mousedown", onPointerDown);
40886
+ document.addEventListener("keydown", onKeyDown);
40887
+ return () => {
40888
+ document.removeEventListener("mousedown", onPointerDown);
40889
+ document.removeEventListener("keydown", onKeyDown);
40890
+ };
40891
+ }, [open, onDismiss]);
40892
+ return ref;
40893
+ };
40894
+ var Radio = ({
40895
+ label,
40896
+ selected,
40897
+ color: color2,
40898
+ disabled,
40899
+ onSelect
40900
+ }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
40901
+ "button",
40902
+ {
40903
+ type: "button",
40904
+ role: "radio",
40905
+ "aria-checked": selected,
40906
+ disabled,
40907
+ onClick: onSelect,
40908
+ style: {
40909
+ display: "flex",
40910
+ alignItems: "center",
40911
+ gap: 5,
40912
+ background: "transparent",
40913
+ border: "none",
40914
+ padding: 0,
40915
+ fontSize: 10,
40916
+ fontFamily: "inherit",
40917
+ color: selected ? color2 : GUI_THEME.dim,
40918
+ cursor: disabled ? "not-allowed" : "pointer",
40919
+ opacity: disabled ? 0.4 : 1
40920
+ },
40921
+ children: [
40922
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40923
+ "span",
40924
+ {
40925
+ style: {
40926
+ width: 12,
40927
+ height: 12,
40928
+ borderRadius: "50%",
40929
+ border: `1px solid ${selected ? color2 : GUI_THEME.dim}`,
40930
+ display: "inline-flex",
40931
+ alignItems: "center",
40932
+ justifyContent: "center",
40933
+ flexShrink: 0
40934
+ },
40935
+ children: selected && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40936
+ "span",
40937
+ {
40938
+ style: {
40939
+ width: 6,
40940
+ height: 6,
40941
+ borderRadius: "50%",
40942
+ background: color2
40943
+ }
40944
+ }
40945
+ )
40946
+ }
40947
+ ),
40948
+ label
40949
+ ]
40950
+ }
40951
+ );
40952
+ var BoardSeriesGroup = ({
40953
+ showIssues,
40954
+ view,
40955
+ identities,
40956
+ hiddenIds,
40957
+ expanded,
40958
+ identitiesExpanded,
40959
+ filtered,
40960
+ onChangeShowIssues,
40961
+ onChangeView,
40962
+ onToggleIdentity,
40963
+ onOnlyIdentity,
40964
+ onToggleExpanded,
40965
+ onSetIdentitiesExpanded
40966
+ }) => {
40967
+ const sole = soleVisibleIdentity(identities, hiddenIds);
40968
+ const partial = filtered && hiddenIds.size > 0 && identityAxisFor(view) !== null;
40969
+ const label = sole ? `${VIEW_LABELS[view]}: ${sole.name}` : partial ? `${VIEW_LABELS[view]} (multi)` : VIEW_LABELS[view];
40970
+ const color2 = sole?.color ?? (partial ? GUI_THEME.dim2 : boardViewColor(view));
40971
+ const ref = useDismissOnOutsideClick(expanded, onToggleExpanded);
40972
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
40973
+ "div",
40974
+ {
40975
+ ref,
40976
+ style: { position: "relative", display: "flex", flexDirection: "column" },
40977
+ children: [
40978
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
40979
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40980
+ Checkbox,
40981
+ {
40982
+ label: null,
40983
+ title: "Show board events",
40984
+ checked: showIssues,
40985
+ activeColor: color2,
40986
+ onChange: onChangeShowIssues
40987
+ }
40988
+ ),
40989
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
40990
+ "button",
40991
+ {
40992
+ type: "button",
40993
+ onClick: onToggleExpanded,
40994
+ disabled: !showIssues,
40995
+ title: filtered ? "Choose what the board series plots" : "This window holds too many events to split by kind",
40996
+ "aria-haspopup": "listbox",
40997
+ "aria-expanded": expanded,
40998
+ style: selectTriggerStyle(color2, !showIssues),
40999
+ children: [
41000
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41001
+ "span",
41002
+ {
41003
+ style: {
41004
+ overflow: "hidden",
41005
+ textOverflow: "ellipsis",
41006
+ whiteSpace: "nowrap"
41007
+ },
41008
+ children: label
41009
+ }
41010
+ ),
41011
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { style: { display: "inline-flex", flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconChevronDown, { size: 12 }) })
41012
+ ]
41013
+ }
41014
+ )
41015
+ ] }),
41016
+ expanded && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { role: "radiogroup", style: popoverStyle, children: BOARD_VIEWS.map((option) => {
41017
+ const selected = view === option;
41018
+ const hasList = identityAxisFor(option) !== null;
41019
+ const open = selected && identitiesExpanded;
41020
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
41021
+ "div",
41022
+ {
41023
+ style: { display: "flex", flexDirection: "column", gap: 7 },
41024
+ children: [
41025
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 3 }, children: [
41026
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41027
+ Radio,
41028
+ {
41029
+ label: VIEW_LABELS[option],
41030
+ selected,
41031
+ color: boardViewColor(option),
41032
+ disabled: !showIssues || !filtered,
41033
+ onSelect: () => onChangeView(option)
41034
+ }
41035
+ ),
41036
+ hasList && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41037
+ "button",
41038
+ {
41039
+ type: "button",
41040
+ disabled: !showIssues || !filtered,
41041
+ onClick: () => {
41042
+ if (!selected) onChangeView(option);
41043
+ onSetIdentitiesExpanded(!open);
41044
+ },
41045
+ title: open ? "Hide the list" : "Pick which to show",
41046
+ "aria-expanded": open,
41047
+ style: disclosureStyle,
41048
+ children: open ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconChevronDown, { size: 12 }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconChevronRight, { size: 12 })
41049
+ }
41050
+ )
41051
+ ] }),
41052
+ open && identities.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41053
+ "div",
41054
+ {
41055
+ style: {
41056
+ ...nestedListStyle,
41057
+ // A repo with dozens of tags would otherwise push the
41058
+ // board itself off the screen.
41059
+ maxHeight: 132,
41060
+ overflowY: "auto"
41061
+ },
41062
+ children: identities.map((identity) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
41063
+ "div",
41064
+ {
41065
+ style: {
41066
+ display: "flex",
41067
+ alignItems: "center",
41068
+ justifyContent: "space-between",
41069
+ gap: 10
41070
+ },
41071
+ children: [
41072
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41073
+ Checkbox,
41074
+ {
41075
+ label: identity.name,
41076
+ checked: !hiddenIds.has(identity.id),
41077
+ activeColor: identity.color,
41078
+ disabled: !showIssues,
41079
+ onChange: (next) => onToggleIdentity(identity.id, next)
41080
+ }
41081
+ ),
41082
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41083
+ "button",
41084
+ {
41085
+ type: "button",
41086
+ title: `Show only ${identity.name}`,
41087
+ disabled: !showIssues,
41088
+ onClick: () => onOnlyIdentity(identity.id),
41089
+ style: onlyButtonStyle,
41090
+ children: "only"
41091
+ }
41092
+ )
41093
+ ]
41094
+ },
41095
+ identity.id
41096
+ ))
41097
+ }
41098
+ )
41099
+ ]
41100
+ },
41101
+ option
41102
+ );
41103
+ }) })
41104
+ ]
41105
+ }
41106
+ );
41107
+ };
40584
41108
  var navButtonStyle = {
40585
41109
  background: "transparent",
40586
41110
  border: `1px solid ${GUI_THEME.dim}`,
@@ -40599,167 +41123,173 @@ var ScrubberControls = ({
40599
41123
  showIssues,
40600
41124
  showCommits,
40601
41125
  allBoards,
41126
+ boardView,
41127
+ identities,
41128
+ hiddenIdentityIds,
41129
+ categoriesExpanded,
41130
+ identitiesExpanded,
41131
+ categoriesFiltered,
40602
41132
  isScrubbing,
40603
- nowLabel,
41133
+ onReturnToLive,
40604
41134
  onChangeScope,
40605
41135
  onChangeOffset,
40606
41136
  onChangeLayoutMode,
40607
41137
  onChangeShowIssues,
40608
41138
  onChangeShowCommits,
40609
41139
  onChangeAllBoards,
40610
- onReturnToLive
40611
- }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
40612
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
40613
- scope !== "all" && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 2 }, children: [
40614
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40615
- "button",
40616
- {
40617
- title: "Earlier",
40618
- onClick: () => onChangeOffset(offset + 1),
40619
- style: navButtonStyle,
40620
- children: "\u25C0"
40621
- }
40622
- ),
40623
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40624
- "span",
40625
- {
40626
- style: {
40627
- fontSize: 10,
40628
- color: GUI_THEME.dim,
40629
- whiteSpace: "nowrap",
40630
- overflow: "hidden",
40631
- // Fixed, not min, so the changing label never shifts the
40632
- // buttons around it.
40633
- width: 88,
40634
- flexShrink: 0,
40635
- textAlign: "center"
40636
- },
40637
- children: formatPeriodLabel(scope, offset, periodRange)
40638
- }
40639
- ),
40640
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40641
- "button",
40642
- {
40643
- title: "Later",
40644
- disabled: offset === 0,
40645
- onClick: () => onChangeOffset(Math.max(0, offset - 1)),
40646
- style: {
40647
- ...navButtonStyle,
40648
- opacity: offset === 0 ? 0.35 : 1,
40649
- cursor: offset === 0 ? "default" : "pointer"
41140
+ onChangeBoardView,
41141
+ onToggleIdentity,
41142
+ onOnlyIdentity,
41143
+ onToggleCategoriesExpanded,
41144
+ onSetIdentitiesExpanded
41145
+ }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
41146
+ "div",
41147
+ {
41148
+ style: {
41149
+ display: "flex",
41150
+ alignItems: "center",
41151
+ justifyContent: "flex-end",
41152
+ gap: 12
41153
+ },
41154
+ children: [
41155
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
41156
+ scope !== "all" && /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 2 }, children: [
41157
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41158
+ "button",
41159
+ {
41160
+ title: "Earlier",
41161
+ onClick: () => onChangeOffset(offset + 1),
41162
+ style: navButtonStyle,
41163
+ children: "\u25C0"
41164
+ }
41165
+ ),
41166
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41167
+ "span",
41168
+ {
41169
+ style: {
41170
+ fontSize: 10,
41171
+ color: GUI_THEME.dim,
41172
+ whiteSpace: "nowrap",
41173
+ overflow: "hidden",
41174
+ // Fixed, not min, so the changing label never shifts the
41175
+ // buttons around it.
41176
+ width: 88,
41177
+ flexShrink: 0,
41178
+ textAlign: "center"
41179
+ },
41180
+ children: formatPeriodLabel(scope, offset, periodRange)
41181
+ }
41182
+ ),
41183
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41184
+ "button",
41185
+ {
41186
+ title: "Later",
41187
+ disabled: offset === 0,
41188
+ onClick: () => onChangeOffset(Math.max(0, offset - 1)),
41189
+ style: {
41190
+ ...navButtonStyle,
41191
+ opacity: offset === 0 ? 0.35 : 1,
41192
+ cursor: offset === 0 ? "default" : "pointer"
41193
+ },
41194
+ children: "\u25B6"
41195
+ }
41196
+ )
41197
+ ] }),
41198
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { style: { display: "flex", gap: 2 }, children: SCOPES.map((option) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41199
+ "button",
41200
+ {
41201
+ onClick: () => onChangeScope(option),
41202
+ style: toggleButtonStyle(scope === option),
41203
+ children: scopeButtonLabel(option)
40650
41204
  },
40651
- children: "\u25B6"
40652
- }
40653
- )
40654
- ] }),
40655
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { display: "flex", gap: 2 }, children: SCOPES.map((option) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40656
- "button",
40657
- {
40658
- onClick: () => onChangeScope(option),
40659
- style: toggleButtonStyle(scope === option),
40660
- children: scopeButtonLabel(option)
40661
- },
40662
- option
40663
- )) })
40664
- ] }),
40665
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", gap: 2 }, children: [
40666
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40667
- "button",
40668
- {
40669
- title: "How much happened, per equal-width period \u2014 no empty gaps for quiet stretches",
40670
- onClick: () => onChangeLayoutMode("even"),
40671
- style: toggleButtonStyle(layoutMode === "even"),
40672
- children: "Volume"
40673
- }
40674
- ),
40675
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40676
- "button",
40677
- {
40678
- title: "Individual events by exact moment \u2014 x is elapsed time, y is time of day",
40679
- onClick: () => onChangeLayoutMode("real"),
40680
- style: toggleButtonStyle(layoutMode === "real"),
40681
- children: "Events"
40682
- }
40683
- )
40684
- ] }),
40685
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", gap: 10 }, children: [
40686
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40687
- Checkbox,
40688
- {
40689
- label: "Board",
40690
- checked: showIssues,
40691
- activeColor: GUI_THEME.accent,
40692
- onChange: onChangeShowIssues
40693
- }
40694
- ),
40695
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40696
- Checkbox,
40697
- {
40698
- label: "Code",
40699
- checked: showCommits,
40700
- activeColor: GUI_THEME.green,
40701
- onChange: onChangeShowCommits
40702
- }
40703
- ),
40704
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40705
- Checkbox,
40706
- {
40707
- label: "All boards",
40708
- checked: allBoards,
40709
- activeColor: GUI_THEME.accent,
40710
- onChange: onChangeAllBoards
40711
- }
40712
- )
40713
- ] }),
40714
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40715
- "div",
40716
- {
40717
- style: {
40718
- display: "flex",
40719
- justifyContent: "flex-end",
40720
- alignItems: "center",
40721
- // Fixed, not min, so swapping between "Now" and "Return to live"
40722
- // never resizes the controls row.
40723
- width: 100,
40724
- flexShrink: 0
40725
- },
40726
- children: isScrubbing ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41205
+ option
41206
+ )) })
41207
+ ] }),
41208
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { display: "flex", gap: 2 }, children: [
41209
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41210
+ "button",
41211
+ {
41212
+ title: "Volume \u2014 how much happened, per equal-width period, with no empty gaps for quiet stretches",
41213
+ "aria-label": "Volume",
41214
+ "aria-pressed": layoutMode === "even",
41215
+ onClick: () => onChangeLayoutMode("even"),
41216
+ style: iconToggleButtonStyle(layoutMode === "even"),
41217
+ children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconBars, { size: 13 })
41218
+ }
41219
+ ),
41220
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41221
+ "button",
41222
+ {
41223
+ title: "Events \u2014 individual events by exact moment, x is elapsed time and y is time of day",
41224
+ "aria-label": "Events",
41225
+ "aria-pressed": layoutMode === "real",
41226
+ onClick: () => onChangeLayoutMode("real"),
41227
+ style: iconToggleButtonStyle(layoutMode === "real"),
41228
+ children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconScatter, { size: 13 })
41229
+ }
41230
+ )
41231
+ ] }),
41232
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { display: "flex", gap: 12, alignItems: "center" }, children: [
41233
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41234
+ Checkbox,
41235
+ {
41236
+ label: "Code",
41237
+ checked: showCommits,
41238
+ activeColor: GUI_THEME.green,
41239
+ onChange: onChangeShowCommits
41240
+ }
41241
+ ),
41242
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41243
+ BoardSeriesGroup,
41244
+ {
41245
+ showIssues,
41246
+ view: boardView,
41247
+ identities,
41248
+ hiddenIds: hiddenIdentityIds,
41249
+ expanded: categoriesExpanded,
41250
+ identitiesExpanded,
41251
+ filtered: categoriesFiltered,
41252
+ onChangeShowIssues,
41253
+ onChangeView: onChangeBoardView,
41254
+ onToggleIdentity,
41255
+ onOnlyIdentity,
41256
+ onToggleExpanded: onToggleCategoriesExpanded,
41257
+ onSetIdentitiesExpanded
41258
+ }
41259
+ )
41260
+ ] }),
41261
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40727
41262
  "button",
40728
41263
  {
40729
41264
  onClick: onReturnToLive,
41265
+ disabled: !isScrubbing,
41266
+ title: isScrubbing ? "Leave history and follow the board again" : void 0,
40730
41267
  style: {
40731
41268
  background: "transparent",
40732
- border: `1px solid ${GUI_THEME.accent}`,
40733
- color: GUI_THEME.accent,
41269
+ border: `1px solid ${isScrubbing ? GUI_THEME.accent : GUI_THEME.transparent}`,
41270
+ color: isScrubbing ? GUI_THEME.accent : GUI_THEME.dim,
40734
41271
  borderRadius: 6,
40735
- fontSize: 11,
41272
+ fontFamily: "inherit",
41273
+ width: 60,
41274
+ display: "inline-flex",
41275
+ alignItems: "center",
41276
+ justifyContent: isScrubbing ? "center" : "flex-end",
41277
+ fontSize: 10,
40736
41278
  padding: "2px 8px",
40737
- cursor: "pointer",
40738
- whiteSpace: "nowrap"
40739
- },
40740
- children: "Return to live"
40741
- }
40742
- ) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40743
- "span",
40744
- {
40745
- style: {
40746
- fontSize: 11,
40747
- color: GUI_THEME.dim,
40748
- overflow: "hidden",
40749
- textAlign: "right",
40750
- whiteSpace: "nowrap"
41279
+ cursor: isScrubbing ? "pointer" : "default",
41280
+ whiteSpace: "nowrap",
41281
+ flexShrink: 0
40751
41282
  },
40752
- children: nowLabel
41283
+ children: isScrubbing ? "Resume" : "Now"
40753
41284
  }
40754
41285
  )
40755
- }
40756
- )
40757
- ] });
41286
+ ]
41287
+ }
41288
+ );
40758
41289
  var ScrubberHeader = ({
40759
41290
  collapsed,
40760
- onToggleCollapsed,
40761
- scrubbingAsOf
40762
- }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
41291
+ onToggleCollapsed
41292
+ }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40763
41293
  "div",
40764
41294
  {
40765
41295
  style: {
@@ -40769,34 +41299,26 @@ var ScrubberHeader = ({
40769
41299
  fontSize: 11,
40770
41300
  whiteSpace: "nowrap"
40771
41301
  },
40772
- children: [
40773
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40774
- "button",
40775
- {
40776
- onClick: onToggleCollapsed,
40777
- title: collapsed ? "Show time travel" : "Hide time travel",
40778
- style: {
40779
- background: "transparent",
40780
- border: "none",
40781
- color: GUI_THEME.dim,
40782
- fontSize: 11,
40783
- padding: 0,
40784
- cursor: "pointer",
40785
- display: "flex",
40786
- alignItems: "center",
40787
- gap: 4
40788
- },
40789
- children: [
40790
- "Time travel",
40791
- collapsed ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(IconChevronRight, { size: 12 }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(IconChevronDown, { size: 12 })
40792
- ]
40793
- }
40794
- ),
40795
- scrubbingAsOf !== null && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
40796
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { style: { color: GUI_THEME.accent, fontWeight: 700 }, children: "Read-only" }),
40797
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { style: { color: GUI_THEME.primary }, children: scrubbingAsOf })
40798
- ] })
40799
- ]
41302
+ children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41303
+ "button",
41304
+ {
41305
+ onClick: onToggleCollapsed,
41306
+ title: collapsed ? "Show time travel" : "Hide time travel",
41307
+ "aria-label": collapsed ? "Show time travel" : "Hide time travel",
41308
+ "aria-expanded": !collapsed,
41309
+ style: {
41310
+ background: GUI_THEME.panel2,
41311
+ border: `1px solid ${GUI_THEME.line}`,
41312
+ borderRadius: 6,
41313
+ color: GUI_THEME.secondary,
41314
+ padding: "3px 7px",
41315
+ cursor: "pointer",
41316
+ display: "inline-flex",
41317
+ alignItems: "center"
41318
+ },
41319
+ children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconChevronRight, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconChevronDown, { size: 14 })
41320
+ }
41321
+ )
40800
41322
  }
40801
41323
  );
40802
41324
  var SegmentHighlight = ({
@@ -40804,7 +41326,7 @@ var SegmentHighlight = ({
40804
41326
  fractionForTime
40805
41327
  }) => {
40806
41328
  const left = fractionForTime(segment.start);
40807
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41329
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40808
41330
  "div",
40809
41331
  {
40810
41332
  style: {
@@ -40836,7 +41358,7 @@ var SegmentHighlight = ({
40836
41358
  var TrackBaseline = ({
40837
41359
  color: color2,
40838
41360
  anchor
40839
- }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41361
+ }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40840
41362
  "div",
40841
41363
  {
40842
41364
  style: {
@@ -40852,11 +41374,11 @@ var TrackBaseline = ({
40852
41374
  }
40853
41375
  }
40854
41376
  );
40855
- var HourAxisLabels = () => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_jsx_runtime30.Fragment, { children: [
41377
+ var HourAxisLabels = () => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_jsx_runtime32.Fragment, { children: [
40856
41378
  ["00:00", { top: EVENTS_MODE_VERTICAL_PADDING }],
40857
41379
  ["12:00", { top: "50%", transform: "translateY(-50%)" }],
40858
41380
  ["24:00", { bottom: EVENTS_MODE_VERTICAL_PADDING }]
40859
- ].map(([label, position3]) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41381
+ ].map(([label, position3]) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40860
41382
  "span",
40861
41383
  {
40862
41384
  style: {
@@ -40874,14 +41396,18 @@ var HourAxisLabels = () => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_
40874
41396
  var SeriesLayer = ({
40875
41397
  animate,
40876
41398
  children
40877
- }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41399
+ }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40878
41400
  "div",
40879
41401
  {
40880
41402
  style: {
40881
41403
  position: "absolute",
40882
41404
  inset: 0,
40883
41405
  pointerEvents: "none",
40884
- animation: animate ? FADE_IN_ANIMATION : void 0
41406
+ animation: animate ? FADE_IN_ANIMATION : void 0,
41407
+ // Walls this subtree off from the needle and highlight moving above
41408
+ // it: without containment every frame of a drag re-lays-out the
41409
+ // thousands of dots inside, and the drag stutters.
41410
+ contain: "layout paint"
40885
41411
  },
40886
41412
  children
40887
41413
  }
@@ -40904,8 +41430,8 @@ var VolumeBars = ({
40904
41430
  const timeout = setTimeout(() => setFresh(false), BAR_ENTRANCE_TOTAL_MS);
40905
41431
  return () => clearTimeout(timeout);
40906
41432
  }, []);
40907
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
40908
- highlightedIndex !== null && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41433
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
41434
+ highlightedIndex !== null && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40909
41435
  "div",
40910
41436
  {
40911
41437
  style: {
@@ -40921,7 +41447,7 @@ var VolumeBars = ({
40921
41447
  }
40922
41448
  }
40923
41449
  ),
40924
- bars.map(({ index: index2, intensity }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41450
+ bars.map(({ index: index2, intensity }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40925
41451
  "div",
40926
41452
  {
40927
41453
  style: {
@@ -40942,55 +41468,14 @@ var VolumeBars = ({
40942
41468
  ))
40943
41469
  ] });
40944
41470
  };
40945
- var ScatterDot = ({
40946
- fraction,
40947
- hourFraction,
40948
- size,
40949
- color: color2,
40950
- opacity,
40951
- zIndex,
40952
- title,
40953
- animation,
40954
- interactive = true,
40955
- onMouseEnter,
40956
- onMouseLeave,
40957
- onClick
40958
- }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40959
- "div",
40960
- {
40961
- title,
40962
- onPointerDown: onClick && ((event) => event.stopPropagation()),
40963
- onClick: onClick && ((event) => {
40964
- event.stopPropagation();
40965
- onClick();
40966
- }),
40967
- onMouseEnter,
40968
- onMouseLeave,
40969
- style: {
40970
- position: "absolute",
40971
- left: `${fraction * 100}%`,
40972
- top: EVENTS_MODE_VERTICAL_PADDING + hourFraction * EVENTS_SCATTER_HEIGHT,
40973
- width: size,
40974
- height: size,
40975
- borderRadius: "50%",
40976
- background: color2,
40977
- opacity,
40978
- zIndex,
40979
- transform: `translate(${-size / 2}px, -50%)`,
40980
- animation,
40981
- pointerEvents: interactive ? "auto" : "none",
40982
- cursor: onClick ? "pointer" : void 0
40983
- }
40984
- }
40985
- );
40986
41471
  var ScrubberNeedle = ({ fraction }) => {
40987
41472
  const [hovered, setHovered] = (0, import_react14.useState)(false);
40988
41473
  const hover = {
40989
41474
  onMouseEnter: () => setHovered(true),
40990
41475
  onMouseLeave: () => setHovered(false)
40991
41476
  };
40992
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
40993
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41477
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
41478
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40994
41479
  "div",
40995
41480
  {
40996
41481
  ...hover,
@@ -41010,7 +41495,7 @@ var ScrubberNeedle = ({ fraction }) => {
41010
41495
  }
41011
41496
  }
41012
41497
  ),
41013
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41498
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41014
41499
  "div",
41015
41500
  {
41016
41501
  ...hover,
@@ -41040,7 +41525,7 @@ var ScrubberHoverHint = ({
41040
41525
  fraction,
41041
41526
  trackWidthPx,
41042
41527
  empty: empty3 = false
41043
- }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
41528
+ }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
41044
41529
  "div",
41045
41530
  {
41046
41531
  style: {
@@ -41072,8 +41557,8 @@ var ScrubberHoverHint = ({
41072
41557
  zIndex: 5
41073
41558
  },
41074
41559
  children: [
41075
- segmentLabel && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { fontSize: 10, color: GUI_THEME.dim }, children: segmentLabel }),
41076
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41560
+ segmentLabel && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { style: { fontSize: 10, color: GUI_THEME.dim }, children: segmentLabel }),
41561
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41077
41562
  "div",
41078
41563
  {
41079
41564
  style: {
@@ -41086,7 +41571,7 @@ var ScrubberHoverHint = ({
41086
41571
  children: label
41087
41572
  }
41088
41573
  ),
41089
- rows.map((row, index2) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41574
+ rows.map((row, index2) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41090
41575
  "div",
41091
41576
  {
41092
41577
  style: {
@@ -41102,19 +41587,183 @@ var ScrubberHoverHint = ({
41102
41587
  ]
41103
41588
  }
41104
41589
  );
41590
+ var HIT_RADIUS = 7;
41591
+ var pointAt = (layers, x, y, width) => {
41592
+ let best = null;
41593
+ let bestDistance = HIT_RADIUS * HIT_RADIUS;
41594
+ for (const layer of layers) {
41595
+ if (layer.leaving) continue;
41596
+ for (const point4 of layer.points) {
41597
+ const dx = point4.fraction * width - x;
41598
+ const dy = EVENTS_MODE_VERTICAL_PADDING + point4.hourFraction * EVENTS_SCATTER_HEIGHT - y;
41599
+ const distance = dx * dx + dy * dy;
41600
+ if (distance <= bestDistance) {
41601
+ bestDistance = distance;
41602
+ best = point4;
41603
+ }
41604
+ }
41605
+ }
41606
+ return best;
41607
+ };
41608
+ var ScatterCanvas = ({
41609
+ layers,
41610
+ animate,
41611
+ onPointEnter,
41612
+ onPointLeave,
41613
+ onInspectCommit
41614
+ }) => {
41615
+ const canvasRef = (0, import_react14.useRef)(null);
41616
+ const sizeRef = (0, import_react14.useRef)({ width: 0, height: 0 });
41617
+ const layersRef = (0, import_react14.useRef)(layers);
41618
+ layersRef.current = layers;
41619
+ const phasesRef = (0, import_react14.useRef)(/* @__PURE__ */ new Map());
41620
+ const frameRef = (0, import_react14.useRef)(null);
41621
+ const hoveredRef = (0, import_react14.useRef)(null);
41622
+ const [entrancePlaying, setEntrancePlaying] = (0, import_react14.useState)(false);
41623
+ const paint = (0, import_react14.useCallback)((now) => {
41624
+ const canvas = canvasRef.current;
41625
+ const context = canvas?.getContext("2d");
41626
+ if (!canvas || !context) return false;
41627
+ const { width, height } = sizeRef.current;
41628
+ context.clearRect(0, 0, width, height);
41629
+ let running = false;
41630
+ for (const layer of layersRef.current) {
41631
+ const phase = phasesRef.current.get(layer.id);
41632
+ const elapsed = phase ? now - phase.startedAt : null;
41633
+ const done = elapsed === null || elapsed >= DOT_EXIT_TOTAL_MS;
41634
+ if (phase && !done) running = true;
41635
+ if (phase?.mode === "out" && done) continue;
41636
+ for (const point4 of layer.points) {
41637
+ const scale = phase === null || phase === void 0 || done ? 1 : phase.mode === "in" ? dotEntranceScale(point4.key, elapsed) : dotExitScale(point4.key, elapsed);
41638
+ if (scale <= 0) continue;
41639
+ context.globalAlpha = point4.opacity;
41640
+ context.fillStyle = point4.color;
41641
+ context.beginPath();
41642
+ context.arc(
41643
+ point4.fraction * width,
41644
+ EVENTS_MODE_VERTICAL_PADDING + point4.hourFraction * EVENTS_SCATTER_HEIGHT,
41645
+ point4.radius * scale,
41646
+ 0,
41647
+ Math.PI * 2
41648
+ );
41649
+ context.fill();
41650
+ }
41651
+ }
41652
+ context.globalAlpha = 1;
41653
+ return running;
41654
+ }, []);
41655
+ const run = (0, import_react14.useCallback)(() => {
41656
+ if (frameRef.current !== null) return;
41657
+ const step = () => {
41658
+ frameRef.current = null;
41659
+ const running = paint(performance.now());
41660
+ if (running) frameRef.current = requestAnimationFrame(step);
41661
+ else setEntrancePlaying(false);
41662
+ };
41663
+ frameRef.current = requestAnimationFrame(step);
41664
+ }, [paint]);
41665
+ (0, import_react14.useEffect)(() => {
41666
+ const canvas = canvasRef.current;
41667
+ const parent = canvas?.parentElement;
41668
+ if (!canvas || !parent) return;
41669
+ const resize = () => {
41670
+ const ratio = window.devicePixelRatio || 1;
41671
+ const { width, height } = parent.getBoundingClientRect();
41672
+ sizeRef.current = { width, height };
41673
+ canvas.width = Math.round(width * ratio);
41674
+ canvas.height = Math.round(height * ratio);
41675
+ canvas.style.width = `${width}px`;
41676
+ canvas.style.height = `${height}px`;
41677
+ canvas.getContext("2d")?.setTransform(ratio, 0, 0, ratio, 0, 0);
41678
+ paint(performance.now());
41679
+ };
41680
+ resize();
41681
+ const observer = new ResizeObserver(resize);
41682
+ observer.observe(parent);
41683
+ return () => observer.disconnect();
41684
+ }, [paint]);
41685
+ const signature = layers.map((layer) => `${layer.id}:${layer.generation}:${layer.leaving}`).join("|");
41686
+ (0, import_react14.useEffect)(() => {
41687
+ const seen = /* @__PURE__ */ new Set();
41688
+ for (const layer of layers) {
41689
+ seen.add(layer.id);
41690
+ const key = `${layer.generation}:${layer.leaving}`;
41691
+ const marker = `${layer.id}@${key}`;
41692
+ const current = phasesRef.current.get(layer.id);
41693
+ if (current?.marker === marker)
41694
+ continue;
41695
+ if (!animate) {
41696
+ phasesRef.current.delete(layer.id);
41697
+ continue;
41698
+ }
41699
+ phasesRef.current.set(layer.id, {
41700
+ mode: layer.leaving ? "out" : "in",
41701
+ startedAt: performance.now(),
41702
+ marker
41703
+ });
41704
+ if (!layer.leaving) setEntrancePlaying(true);
41705
+ }
41706
+ for (const id of [...phasesRef.current.keys()])
41707
+ if (!seen.has(id)) phasesRef.current.delete(id);
41708
+ if (animate) run();
41709
+ else paint(performance.now());
41710
+ }, [signature, animate, run, paint]);
41711
+ (0, import_react14.useEffect)(() => {
41712
+ if (frameRef.current === null) paint(performance.now());
41713
+ }, [layers, paint]);
41714
+ (0, import_react14.useEffect)(
41715
+ () => () => {
41716
+ if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
41717
+ },
41718
+ []
41719
+ );
41720
+ const hitTest = (event) => {
41721
+ const rect = event.currentTarget.getBoundingClientRect();
41722
+ return pointAt(
41723
+ layersRef.current,
41724
+ event.clientX - rect.left,
41725
+ event.clientY - rect.top,
41726
+ rect.width
41727
+ );
41728
+ };
41729
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41730
+ "canvas",
41731
+ {
41732
+ ref: canvasRef,
41733
+ "data-entrance": entrancePlaying ? "playing" : "done",
41734
+ style: { position: "absolute", inset: 0 },
41735
+ onMouseMove: (event) => {
41736
+ const point4 = hitTest(event);
41737
+ if (point4?.key === hoveredRef.current) return;
41738
+ hoveredRef.current = point4?.key ?? null;
41739
+ if (point4) onPointEnter(point4);
41740
+ else onPointLeave();
41741
+ },
41742
+ onMouseLeave: () => {
41743
+ hoveredRef.current = null;
41744
+ onPointLeave();
41745
+ },
41746
+ onPointerDown: (event) => {
41747
+ if (hitTest(event)?.commitSha) event.stopPropagation();
41748
+ },
41749
+ onClick: (event) => {
41750
+ const sha = hitTest(event)?.commitSha;
41751
+ if (sha) onInspectCommit(sha);
41752
+ }
41753
+ }
41754
+ );
41755
+ };
41105
41756
 
41106
41757
  // source/gui/client/components/ScrubberLayout.tsx
41107
- var import_jsx_runtime31 = __toESM(require_jsx_runtime(), 1);
41108
- var dotAnimation = (key, animate, leaving) => !animate ? void 0 : leaving ? dotExitAnimation(key) : dotAppearAnimation(key);
41758
+ var import_jsx_runtime33 = __toESM(require_jsx_runtime(), 1);
41109
41759
  var ScrubberLayout = ({
41110
41760
  collapsed,
41111
41761
  onToggleCollapsed,
41112
- scrubbingAsOf,
41113
41762
  controls,
41114
41763
  chart
41115
41764
  }) => {
41116
41765
  const { axis, layoutMode, animate, windowKey, on } = chart;
41117
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
41766
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
41118
41767
  Panel,
41119
41768
  {
41120
41769
  as: "div",
@@ -41131,8 +41780,8 @@ var ScrubberLayout = ({
41131
41780
  overflow: "visible"
41132
41781
  },
41133
41782
  children: [
41134
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("style", { children: SCRUBBER_KEYFRAMES }),
41135
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
41783
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("style", { children: SCRUBBER_KEYFRAMES }),
41784
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
41136
41785
  "div",
41137
41786
  {
41138
41787
  style: {
@@ -41141,27 +41790,26 @@ var ScrubberLayout = ({
41141
41790
  gap: collapsed ? 0 : 8
41142
41791
  },
41143
41792
  children: [
41144
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
41793
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
41145
41794
  "div",
41146
41795
  {
41147
41796
  style: {
41148
41797
  display: "flex",
41149
41798
  alignItems: "center",
41150
41799
  justifyContent: "space-between",
41151
- // Fixed whether or not the banner renders, so the track below
41152
- // never shifts vertically.
41800
+ // Holds the row's height when collapsing takes the controls out
41801
+ // of it.
41153
41802
  minHeight: 22
41154
41803
  },
41155
41804
  children: [
41156
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41805
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
41157
41806
  ScrubberHeader,
41158
41807
  {
41159
41808
  collapsed,
41160
- onToggleCollapsed,
41161
- scrubbingAsOf
41809
+ onToggleCollapsed
41162
41810
  }
41163
41811
  ),
41164
- !collapsed && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ScrubberControls, { ...controls })
41812
+ !collapsed && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(ScrubberControls, { ...controls })
41165
41813
  ]
41166
41814
  }
41167
41815
  ),
@@ -41169,7 +41817,7 @@ var ScrubberLayout = ({
41169
41817
  // spanning them and the gap between. Pointer handlers belong here
41170
41818
  // rather than on either chart, so a drag or hover anywhere across
41171
41819
  // the pair — the gap included — counts as one timeline.
41172
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
41820
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
41173
41821
  "div",
41174
41822
  {
41175
41823
  onPointerDown: on.onPointerDown,
@@ -41186,14 +41834,14 @@ var ScrubberLayout = ({
41186
41834
  cursor: "pointer"
41187
41835
  },
41188
41836
  children: [
41189
- chart.hoveredSegment && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41837
+ chart.hoveredSegment && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
41190
41838
  SegmentHighlight,
41191
41839
  {
41192
41840
  segment: chart.hoveredSegment,
41193
41841
  fractionForTime: axis.fractionForTime
41194
41842
  }
41195
41843
  ),
41196
- /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
41844
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
41197
41845
  "div",
41198
41846
  {
41199
41847
  ref: chart.trackRef,
@@ -41208,15 +41856,15 @@ var ScrubberLayout = ({
41208
41856
  alignItems: "center"
41209
41857
  },
41210
41858
  children: [
41211
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41859
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
41212
41860
  TrackBaseline,
41213
41861
  {
41214
- color: GUI_THEME.accent,
41862
+ color: chart.issueSeriesColor,
41215
41863
  anchor: layoutMode === "even" ? "bottom" : "centre"
41216
41864
  }
41217
41865
  ),
41218
- layoutMode === "real" && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(HourAxisLabels, {}),
41219
- chart.showIssues && layoutMode === "even" && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(SeriesLayer, { animate, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41866
+ layoutMode === "real" && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(HourAxisLabels, {}),
41867
+ chart.showIssues && layoutMode === "even" && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(SeriesLayer, { animate, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
41220
41868
  VolumeBars,
41221
41869
  {
41222
41870
  bars: chart.issueBars,
@@ -41224,75 +41872,25 @@ var ScrubberLayout = ({
41224
41872
  firstBar: chart.issueBarRange[0],
41225
41873
  lastBar: chart.issueBarRange[1],
41226
41874
  highlightedIndex: chart.hoveredBucketIndex,
41227
- color: GUI_THEME.accent,
41875
+ color: chart.issueSeriesColor,
41228
41876
  direction: "up",
41229
41877
  animate
41230
41878
  }
41231
41879
  ) }, `issues-${windowKey}`),
41232
- chart.issueScatter.mounted && layoutMode === "real" && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41233
- SeriesLayer,
41234
- {
41235
- animate: animate && !chart.issueScatter.leaving,
41236
- children: chart.eventBuckets.map((bucket) => {
41237
- const intensity = bucket.count / chart.maxEventCount;
41238
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41239
- ScatterDot,
41240
- {
41241
- fraction: axis.fractionForTime(bucket.t),
41242
- hourFraction: hourFractionForTime(bucket.t),
41243
- size: 3 + intensity * 6,
41244
- color: GUI_THEME.accent,
41245
- opacity: 0.3 + intensity * 0.5,
41246
- zIndex: 2,
41247
- title: `${bucket.count} change${bucket.count === 1 ? "" : "s"}, ${formatDateTime(new Date(bucket.t))}`,
41248
- animation: dotAnimation(
41249
- String(bucket.t),
41250
- animate,
41251
- chart.issueScatter.leaving
41252
- ),
41253
- interactive: !chart.issueScatter.leaving,
41254
- onMouseEnter: () => on.onIssueDotEnter(bucket),
41255
- onMouseLeave: on.onIssueDotLeave
41256
- },
41257
- bucket.t
41258
- );
41259
- })
41260
- },
41261
- `issues-${windowKey}`
41262
- ),
41263
- chart.commitScatter.mounted && layoutMode === "real" && chart.commits.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41264
- SeriesLayer,
41880
+ layoutMode === "real" && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
41881
+ ScatterCanvas,
41265
41882
  {
41266
- animate: animate && !chart.commitScatter.leaving,
41267
- children: chart.commits.map((commit) => /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41268
- ScatterDot,
41269
- {
41270
- fraction: axis.fractionForTime(commit.time),
41271
- hourFraction: hourFractionForTime(commit.time),
41272
- size: 4,
41273
- color: GUI_THEME.green,
41274
- opacity: chart.hoveredCommitSha === commit.sha ? 1 : 0.55,
41275
- zIndex: 1,
41276
- title: `${formatDateTime(new Date(commit.time))} \u2014 ${commit.subject} \u2014 ${commit.author} (${commit.linesChanged.toLocaleString()} lines)`,
41277
- animation: dotAnimation(
41278
- commit.sha,
41279
- animate,
41280
- chart.commitScatter.leaving
41281
- ),
41282
- interactive: !chart.commitScatter.leaving,
41283
- onClick: () => on.onInspectCommit(commit.sha),
41284
- onMouseEnter: () => on.onCommitDotEnter(commit),
41285
- onMouseLeave: on.onCommitDotLeave
41286
- },
41287
- commit.sha
41288
- ))
41289
- },
41290
- `commits-${windowKey}`
41883
+ layers: chart.scatterLayers,
41884
+ animate,
41885
+ onPointEnter: on.onScatterPointEnter,
41886
+ onPointLeave: on.onScatterPointLeave,
41887
+ onInspectCommit: on.onInspectCommit
41888
+ }
41291
41889
  )
41292
41890
  ]
41293
41891
  }
41294
41892
  ),
41295
- chart.showCommits && layoutMode === "even" && chart.commits.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
41893
+ chart.showCommits && layoutMode === "even" && chart.commits.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
41296
41894
  "div",
41297
41895
  {
41298
41896
  onMouseEnter: on.onCommitTrackMouseEnter,
@@ -41305,8 +41903,8 @@ var ScrubberLayout = ({
41305
41903
  animation: animate ? FADE_IN_ANIMATION : void 0
41306
41904
  },
41307
41905
  children: [
41308
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(TrackBaseline, { color: GUI_THEME.green, anchor: "top" }),
41309
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41906
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(TrackBaseline, { color: GUI_THEME.green, anchor: "top" }),
41907
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
41310
41908
  VolumeBars,
41311
41909
  {
41312
41910
  bars: chart.commitBars,
@@ -41323,17 +41921,17 @@ var ScrubberLayout = ({
41323
41921
  },
41324
41922
  `commits-${windowKey}`
41325
41923
  ),
41326
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ScrubberNeedle, { fraction: chart.thumbFraction }),
41327
- chart.boardHint && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41924
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(ScrubberNeedle, { fraction: chart.thumbFraction }),
41925
+ chart.boardHint && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
41328
41926
  ScrubberHoverHint,
41329
41927
  {
41330
41928
  ...chart.boardHint,
41331
41929
  segmentLabel: chart.hoveredSegment?.label,
41332
- stripeColor: GUI_THEME.accent,
41930
+ stripeColor: chart.issueSeriesColor,
41333
41931
  trackWidthPx: chart.trackWidthPx
41334
41932
  }
41335
41933
  ),
41336
- chart.commitHint && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41934
+ chart.commitHint && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
41337
41935
  ScrubberHoverHint,
41338
41936
  {
41339
41937
  ...chart.commitHint,
@@ -41354,13 +41952,31 @@ var ScrubberLayout = ({
41354
41952
  };
41355
41953
 
41356
41954
  // source/gui/client/components/TimeScrubber.tsx
41357
- var import_jsx_runtime32 = __toESM(require_jsx_runtime(), 1);
41955
+ var import_jsx_runtime34 = __toESM(require_jsx_runtime(), 1);
41358
41956
  var SCRUB_THROTTLE_MS = 120;
41957
+ var boardEventRow = (count) => `${count} board event${count === 1 ? "" : "s"}`;
41359
41958
  var COLLAPSED_STORAGE_KEY = "epiq.timeScrubber.collapsed";
41360
41959
  var SCOPE_STORAGE_KEY = "epiq.timeScrubber.scope";
41361
41960
  var SHOW_ISSUES_STORAGE_KEY = "epiq.timeScrubber.showIssues";
41362
41961
  var SHOW_COMMITS_STORAGE_KEY = "epiq.timeScrubber.showCommits";
41363
41962
  var ALL_BOARDS_STORAGE_KEY = "epiq.timeScrubber.allBoards";
41963
+ var BOARD_VIEW_STORAGE_KEY = "epiq.timeScrubber.boardView";
41964
+ var HIDDEN_IDENTITIES_STORAGE_KEY = "epiq.timeScrubber.hiddenIdentities";
41965
+ var CATEGORIES_EXPANDED_STORAGE_KEY = "epiq.timeScrubber.categoriesExpanded";
41966
+ var IDENTITIES_EXPANDED_STORAGE_KEY = "epiq.timeScrubber.identitiesExpanded";
41967
+ var readStoredBoardView = () => {
41968
+ const stored = localStorage.getItem(BOARD_VIEW_STORAGE_KEY);
41969
+ return isBoardView(stored) ? stored : "all";
41970
+ };
41971
+ var readStoredHiddenIds = () => {
41972
+ try {
41973
+ const stored = localStorage.getItem(HIDDEN_IDENTITIES_STORAGE_KEY);
41974
+ const parsed = stored ? JSON.parse(stored) : [];
41975
+ return new Set(Array.isArray(parsed) ? parsed.map(String) : []);
41976
+ } catch {
41977
+ return /* @__PURE__ */ new Set();
41978
+ }
41979
+ };
41364
41980
  var readStoredScope = () => {
41365
41981
  const stored = localStorage.getItem(SCOPE_STORAGE_KEY);
41366
41982
  return isScope(stored) ? stored : "all";
@@ -41375,7 +41991,8 @@ var TimeScrubber = ({
41375
41991
  onRequestHistory,
41376
41992
  boardId,
41377
41993
  connected,
41378
- onInspectCommit
41994
+ onInspectCommit,
41995
+ onBoardFilterChange
41379
41996
  }) => {
41380
41997
  const animate = !usePrefersReducedMotion();
41381
41998
  const trackRef = (0, import_react15.useRef)(null);
@@ -41396,6 +42013,16 @@ var TimeScrubber = ({
41396
42013
  SHOW_COMMITS_STORAGE_KEY,
41397
42014
  true
41398
42015
  );
42016
+ const [boardView, setBoardView] = (0, import_react15.useState)(readStoredBoardView);
42017
+ const [hiddenIdentityIds, setHiddenIdentityIds] = (0, import_react15.useState)(readStoredHiddenIds);
42018
+ const [categoriesExpanded, setCategoriesExpanded] = usePersistedFlag(
42019
+ CATEGORIES_EXPANDED_STORAGE_KEY,
42020
+ false
42021
+ );
42022
+ const [identitiesExpanded, setIdentitiesExpanded] = usePersistedFlag(
42023
+ IDENTITIES_EXPANDED_STORAGE_KEY,
42024
+ false
42025
+ );
41399
42026
  const [allBoards, setAllBoards] = usePersistedFlag(
41400
42027
  ALL_BOARDS_STORAGE_KEY,
41401
42028
  false
@@ -41425,19 +42052,137 @@ var TimeScrubber = ({
41425
42052
  armEntrance();
41426
42053
  setOffset(nextOffset);
41427
42054
  };
42055
+ const changeBoardView = (next) => {
42056
+ setBoardView(next);
42057
+ localStorage.setItem(BOARD_VIEW_STORAGE_KEY, next);
42058
+ };
42059
+ const isolateIdentity = (id) => {
42060
+ const others = identities.filter((identity) => identity.id !== id).map((identity) => identity.id);
42061
+ setHiddenIdentityIds((previous3) => {
42062
+ const isolated = !previous3.has(id) && others.length === previous3.size && others.every((other) => previous3.has(other));
42063
+ const hidden = isolated ? /* @__PURE__ */ new Set() : new Set(others);
42064
+ localStorage.setItem(
42065
+ HIDDEN_IDENTITIES_STORAGE_KEY,
42066
+ JSON.stringify([...hidden])
42067
+ );
42068
+ return hidden;
42069
+ });
42070
+ };
42071
+ const toggleIdentity = (id, next) => {
42072
+ setHiddenIdentityIds((previous3) => {
42073
+ const hidden = new Set(previous3);
42074
+ if (next) {
42075
+ hidden.delete(id);
42076
+ } else {
42077
+ hidden.add(id);
42078
+ }
42079
+ localStorage.setItem(
42080
+ HIDDEN_IDENTITIES_STORAGE_KEY,
42081
+ JSON.stringify([...hidden])
42082
+ );
42083
+ return hidden;
42084
+ });
42085
+ };
41428
42086
  const changeAllBoards = (next) => {
41429
42087
  armEntrance();
41430
42088
  setAllBoards(next);
41431
42089
  };
41432
- const axis = buildAxis(timeline, commits);
41433
- const issueCounts = bucketIssueCounts(axis, timeline);
41434
- const commitStats = bucketCommitStats(axis, commits);
41435
- const eventBuckets = timeline?.buckets ?? [];
41436
- const maxIssueBucketCount = maxOf(issueCounts, 1);
41437
- const maxEventCount = maxOf(
41438
- eventBuckets.map((bucket) => bucket.count),
41439
- 1
42090
+ const steadyBoardId = (0, import_react15.useRef)(boardId);
42091
+ if (boardId !== null) steadyBoardId.current = boardId;
42092
+ const requestKey = (0, import_react15.useMemo)(
42093
+ () => JSON.stringify([scope, offset, steadyBoardId.current, allBoards]),
42094
+ [scope, offset, steadyBoardId.current, allBoards]
42095
+ );
42096
+ const filterKey = (0, import_react15.useMemo)(
42097
+ () => JSON.stringify([boardView, [...hiddenIdentityIds].sort()]),
42098
+ [boardView, hiddenIdentityIds]
42099
+ );
42100
+ const entrance = (0, import_react15.useRef)(0);
42101
+ const armed = (0, import_react15.useRef)(true);
42102
+ const seenHistoryId = (0, import_react15.useRef)(historyId);
42103
+ const armEntrance = () => {
42104
+ armed.current = true;
42105
+ };
42106
+ const shownRef = (0, import_react15.useRef)({ timeline, commits });
42107
+ const heldKey = (0, import_react15.useRef)(null);
42108
+ const heldHistoryId = (0, import_react15.useRef)(null);
42109
+ const awaiting = (0, import_react15.useRef)(true);
42110
+ if (heldKey.current !== requestKey) {
42111
+ heldKey.current = requestKey;
42112
+ awaiting.current = true;
42113
+ }
42114
+ if (awaiting.current && heldHistoryId.current !== historyId) {
42115
+ heldHistoryId.current = historyId;
42116
+ shownRef.current = { timeline, commits };
42117
+ awaiting.current = false;
42118
+ }
42119
+ if (seenHistoryId.current !== historyId) {
42120
+ seenHistoryId.current = historyId;
42121
+ if (armed.current) {
42122
+ armed.current = false;
42123
+ entrance.current += 1;
42124
+ }
42125
+ }
42126
+ const shown = shownRef.current;
42127
+ const axis = (0, import_react15.useMemo)(() => buildAxis(shown.timeline, shown.commits), [shown]);
42128
+ const categoriesFiltered = (timeline?.events.length ?? 0) > 0;
42129
+ const identities = (0, import_react15.useMemo)(
42130
+ () => listIdentities(shown.timeline, boardView),
42131
+ [shown, boardView]
42132
+ );
42133
+ const soleIdentity = (0, import_react15.useMemo)(
42134
+ () => soleVisibleIdentity(identities, hiddenIdentityIds),
42135
+ [identities, hiddenIdentityIds]
41440
42136
  );
42137
+ const issueCounts = bucketIssueCounts(
42138
+ axis,
42139
+ shown.timeline,
42140
+ boardView,
42141
+ hiddenIdentityIds
42142
+ );
42143
+ const commitStats = bucketCommitStats(axis, shown.commits);
42144
+ const liveEventDots = (0, import_react15.useMemo)(
42145
+ () => buildEventDots(shown.timeline, boardView, hiddenIdentityIds),
42146
+ [shown, boardView, hiddenIdentityIds]
42147
+ );
42148
+ const dragging = dragFraction !== null;
42149
+ const commitPoints = (0, import_react15.useMemo)(
42150
+ () => shown.commits.map((commit) => ({
42151
+ key: commit.sha,
42152
+ t: commit.time,
42153
+ fraction: axis.fractionForTime(commit.time),
42154
+ hourFraction: hourFractionForTime(commit.time),
42155
+ radius: 2,
42156
+ color: GUI_THEME.green,
42157
+ opacity: 0.55,
42158
+ // The hint labels the moment itself, so this is the rest of it.
42159
+ title: `${commit.subject} \u2014 ${commit.author} (${commit.linesChanged.toLocaleString()} lines)`,
42160
+ commitSha: commit.sha
42161
+ })),
42162
+ [shown.commits, axis]
42163
+ );
42164
+ const issuePoints = (0, import_react15.useMemo)(
42165
+ () => liveEventDots.map((dot) => ({
42166
+ key: dot.key,
42167
+ t: dot.t,
42168
+ fraction: axis.fractionForTime(dot.t),
42169
+ hourFraction: hourFractionForTime(dot.t),
42170
+ radius: dot.size / 2,
42171
+ color: dot.color,
42172
+ opacity: dot.opacity,
42173
+ title: dotDetail(dot),
42174
+ commitSha: null
42175
+ })),
42176
+ [liveEventDots, axis]
42177
+ );
42178
+ const boardFilter = (0, import_react15.useMemo)(
42179
+ () => buildBoardFilter(boardView, identities, hiddenIdentityIds),
42180
+ [boardView, identities, hiddenIdentityIds]
42181
+ );
42182
+ (0, import_react15.useEffect)(() => {
42183
+ onBoardFilterChange(boardFilter);
42184
+ }, [boardFilter]);
42185
+ const maxIssueBucketCount = maxOf(issueCounts, 1);
41441
42186
  const maxCommitCount = maxOf(
41442
42187
  Array.from(commitStats.values(), (stats) => stats.count),
41443
42188
  1
@@ -41449,21 +42194,34 @@ var TimeScrubber = ({
41449
42194
  index: index2,
41450
42195
  intensity: stats.count / maxCommitCount
41451
42196
  }));
41452
- const [entrance, setEntrance] = (0, import_react15.useState)(0);
41453
- const armed = (0, import_react15.useRef)(true);
41454
- const seenHistoryId = (0, import_react15.useRef)(historyId);
41455
- const armEntrance = () => {
41456
- armed.current = true;
41457
- };
41458
- (0, import_react15.useEffect)(armEntrance, [boardId]);
42197
+ const armedBoardId = (0, import_react15.useRef)(boardId);
41459
42198
  (0, import_react15.useEffect)(() => {
41460
- if (seenHistoryId.current === historyId) return;
41461
- seenHistoryId.current = historyId;
41462
- if (!armed.current) return;
41463
- armed.current = false;
41464
- setEntrance((generation) => generation + 1);
41465
- }, [historyId]);
41466
- const windowKey = `${layoutMode}-${entrance}`;
42199
+ if (boardId === null || boardId === armedBoardId.current) return;
42200
+ armedBoardId.current = boardId;
42201
+ armEntrance();
42202
+ }, [boardId]);
42203
+ const windowKey = `${layoutMode}-${entrance.current}-${filterKey}`;
42204
+ const scatterLayers = (0, import_react15.useMemo)(
42205
+ () => [
42206
+ ...commitScatter.mounted ? [
42207
+ {
42208
+ id: "commits",
42209
+ points: commitPoints,
42210
+ generation: windowKey,
42211
+ leaving: commitScatter.leaving
42212
+ }
42213
+ ] : [],
42214
+ ...issueScatter.mounted ? [
42215
+ {
42216
+ id: "issues",
42217
+ points: issuePoints,
42218
+ generation: windowKey,
42219
+ leaving: issueScatter.leaving
42220
+ }
42221
+ ] : []
42222
+ ],
42223
+ [commitPoints, issuePoints, commitScatter, issueScatter, windowKey]
42224
+ );
41467
42225
  const confirmedFraction = timeTravel.mode === "scrub" && timeTravel.asOfTime !== null ? axis.fractionForTime(timeTravel.asOfTime) : 1;
41468
42226
  const fractionFromClientX = (clientX) => {
41469
42227
  const track = trackRef.current;
@@ -41493,7 +42251,30 @@ var TimeScrubber = ({
41493
42251
  );
41494
42252
  const bucketTimeAt = (index2) => index2 !== null && index2 < axis.bucketCount ? axis.bucketTimeAt(index2) : void 0;
41495
42253
  const centreFraction = (index2) => (index2 + 0.5) / axis.bucketCount;
41496
- const boardEventRow = (count) => `${count} board event${count === 1 ? "" : "s"}`;
42254
+ const onScatterPointEnter = (0, import_react15.useCallback)(
42255
+ (point4) => {
42256
+ if (point4.commitSha) {
42257
+ const commit = shown.commits.find((c) => c.sha === point4.commitSha);
42258
+ if (commit) {
42259
+ setHoveredCommit({ commit, fraction: point4.fraction });
42260
+ setHoveredEvent(null);
42261
+ }
42262
+ return;
42263
+ }
42264
+ setHoveredCommit(null);
42265
+ setHoveredEvent({
42266
+ label: formatDateTime(new Date(point4.t)),
42267
+ detail: point4.title,
42268
+ t: point4.t,
42269
+ fraction: point4.fraction
42270
+ });
42271
+ },
42272
+ [shown.commits]
42273
+ );
42274
+ const onScatterPointLeave = (0, import_react15.useCallback)(() => {
42275
+ setHoveredEvent(null);
42276
+ setHoveredCommit(null);
42277
+ }, []);
41497
42278
  const hoveredBucketTime = bucketTimeAt(hoveredBucketIndex);
41498
42279
  const hoveredBucketCount = hoveredBucketIndex !== null ? issueCounts[hoveredBucketIndex] : void 0;
41499
42280
  const boardHint = layoutMode === "even" ? hoveredBucketTime !== void 0 && hoveredBucketCount !== void 0 ? {
@@ -41506,7 +42287,7 @@ var TimeScrubber = ({
41506
42287
  empty: hoveredBucketCount === 0
41507
42288
  } : null : hoveredEvent ? {
41508
42289
  label: hoveredEvent.label,
41509
- rows: [boardEventRow(hoveredEvent.count)],
42290
+ rows: [hoveredEvent.detail],
41510
42291
  fraction: hoveredEvent.fraction
41511
42292
  } : null;
41512
42293
  const hoveredCommitStats = hoveredCommitBucketIndex !== null ? commitStats.get(hoveredCommitBucketIndex) : void 0;
@@ -41535,12 +42316,11 @@ var TimeScrubber = ({
41535
42316
  hoveredEvent?.t ?? hoveredCommit?.commit.time ?? (pointerFraction !== null ? axis.fractionToTime(pointerFraction) : void 0)
41536
42317
  )) ?? null;
41537
42318
  const segmentUnit = chooseSegmentUnit(axis.span);
41538
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42319
+ return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
41539
42320
  ScrubberLayout,
41540
42321
  {
41541
42322
  collapsed,
41542
42323
  onToggleCollapsed: () => setCollapsed(!collapsed),
41543
- scrubbingAsOf: timeTravel.mode === "scrub" ? timeTravel.asOfTime ? formatDateTime(new Date(timeTravel.asOfTime)) : "" : null,
41544
42324
  controls: {
41545
42325
  scope,
41546
42326
  offset,
@@ -41549,15 +42329,25 @@ var TimeScrubber = ({
41549
42329
  showIssues,
41550
42330
  showCommits,
41551
42331
  allBoards,
41552
- isScrubbing: timeTravel.mode === "scrub",
41553
- nowLabel: scope === "all" || offset === 0 ? "Now" : formatDateTime(new Date(axis.latest)),
41554
42332
  onChangeScope: changeScope,
41555
42333
  onChangeOffset: changeOffset,
41556
42334
  onChangeLayoutMode: setLayoutMode,
41557
42335
  onChangeShowIssues: setShowIssues,
41558
42336
  onChangeShowCommits: setShowCommits,
41559
42337
  onChangeAllBoards: changeAllBoards,
41560
- onReturnToLive
42338
+ boardView,
42339
+ identities,
42340
+ hiddenIdentityIds,
42341
+ categoriesExpanded,
42342
+ identitiesExpanded,
42343
+ categoriesFiltered,
42344
+ isScrubbing: timeTravel.mode === "scrub",
42345
+ onReturnToLive,
42346
+ onChangeBoardView: changeBoardView,
42347
+ onToggleIdentity: toggleIdentity,
42348
+ onOnlyIdentity: isolateIdentity,
42349
+ onToggleCategoriesExpanded: () => setCategoriesExpanded(!categoriesExpanded),
42350
+ onSetIdentitiesExpanded: setIdentitiesExpanded
41561
42351
  },
41562
42352
  chart: {
41563
42353
  trackRef,
@@ -41573,9 +42363,12 @@ var TimeScrubber = ({
41573
42363
  issueBarRange: populatedRange(issueBars),
41574
42364
  commitBars,
41575
42365
  commitBarRange: populatedRange(commitBars),
41576
- eventBuckets,
41577
- maxEventCount,
41578
- commits,
42366
+ scatterLayers,
42367
+ issueSeriesColor: soleIdentity?.color ?? boardViewColor(boardView),
42368
+ dragging,
42369
+ // Exits still need their animation, so this only silences a series
42370
+ // that has finished arriving.
42371
+ commits: shown.commits,
41579
42372
  hoveredCommitSha: hoveredCommit?.commit.sha ?? null,
41580
42373
  hoveredBucketIndex,
41581
42374
  hoveredCommitBucketIndex,
@@ -41615,18 +42408,8 @@ var TimeScrubber = ({
41615
42408
  setHoveredCommitBucketIndex(bucketIndexFromEvent(event));
41616
42409
  },
41617
42410
  onCommitTrackMouseLeave: () => setHoveredCommitBucketIndex(null),
41618
- onIssueDotEnter: (bucket) => setHoveredEvent({
41619
- label: formatDateTime(new Date(bucket.t)),
41620
- count: bucket.count,
41621
- t: bucket.t,
41622
- fraction: axis.fractionForTime(bucket.t)
41623
- }),
41624
- onIssueDotLeave: () => setHoveredEvent(null),
41625
- onCommitDotEnter: (commit) => setHoveredCommit({
41626
- commit,
41627
- fraction: axis.fractionForTime(commit.time)
41628
- }),
41629
- onCommitDotLeave: () => setHoveredCommit(null),
42411
+ onScatterPointEnter,
42412
+ onScatterPointLeave,
41630
42413
  onInspectCommit
41631
42414
  }
41632
42415
  }
@@ -41717,8 +42500,8 @@ var nodeRef = (id) => id.slice(-NODE_REF_LENGTH).toUpperCase();
41717
42500
  // source/gui/client/lib/gui-state-helper.tsx
41718
42501
  var getResultValue = (payload) => {
41719
42502
  if (!payload) return void 0;
41720
- if (typeof payload === "object" && payload !== null && "value" in payload && payload.value) {
41721
- return payload.value;
42503
+ if (typeof payload === "object" && payload !== null && "value" in payload) {
42504
+ return payload.value ?? void 0;
41722
42505
  }
41723
42506
  if (typeof payload === "object" && payload !== null && "content" in payload && payload.content?.[0]?.text) {
41724
42507
  return JSON.parse(payload.content[0].text).value;
@@ -41910,8 +42693,9 @@ var blobToBase64 = (blob) => new Promise((resolve, reject) => {
41910
42693
  });
41911
42694
 
41912
42695
  // source/gui/client/App.tsx
41913
- var import_jsx_runtime33 = __toESM(require_jsx_runtime(), 1);
41914
- var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42696
+ var import_jsx_runtime35 = __toESM(require_jsx_runtime(), 1);
42697
+ var EMPTY_COMMENTS = {};
42698
+ var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
41915
42699
  "div",
41916
42700
  {
41917
42701
  style: {
@@ -41955,7 +42739,7 @@ var App = () => {
41955
42739
  const [mutationGate] = (0, import_react16.useState)(createMutationGate);
41956
42740
  const selectedTab = searchParams.get("tab") === "comments" ? "comments" : "overview";
41957
42741
  const navigate = useNavigate();
41958
- const selectedBoard = (state && boardId ? findBoard(state, boardId) : null) ?? state?.boards[0] ?? null;
42742
+ const selectedBoard = (state && boardId ? findBoard(state, boardId) : null) ?? state?.boards?.[0] ?? null;
41959
42743
  const selectedBoardId = selectedBoard?.id ?? null;
41960
42744
  const selectedBoardIdRef = (0, import_react16.useRef)(selectedBoardId);
41961
42745
  selectedBoardIdRef.current = selectedBoardId;
@@ -41980,9 +42764,38 @@ var App = () => {
41980
42764
  if (!boardSlug) return;
41981
42765
  void navigate(`/board/${boardSlug}`);
41982
42766
  };
41983
- const commentsByIssueId = state?.commentsByIssueId ?? {};
42767
+ const commentsByIssueId = state?.commentsByIssueId ?? EMPTY_COMMENTS;
42768
+ const [issueDetail, setIssueDetail] = (0, import_react16.useState)(null);
42769
+ const [boardFilter, setBoardFilter] = (0, import_react16.useState)(null);
42770
+ const { visibleSwimlanes, hiddenIssueCount } = (0, import_react16.useMemo)(() => {
42771
+ const swimlanes = selectedBoard?.swimlanes ?? [];
42772
+ if (!boardFilter) return { visibleSwimlanes: swimlanes, hiddenIssueCount: 0 };
42773
+ let hidden = 0;
42774
+ const visible = swimlanes.map((swimlane) => {
42775
+ const issues = swimlane.issues.filter(
42776
+ (issue) => issuePassesBoardFilter(
42777
+ issue,
42778
+ (commentsByIssueId[issue.id] ?? []).map((comment) => comment.author.id),
42779
+ boardFilter
42780
+ )
42781
+ );
42782
+ hidden += swimlane.issues.length - issues.length;
42783
+ return { ...swimlane, issues };
42784
+ });
42785
+ return { visibleSwimlanes: visible, hiddenIssueCount: hidden };
42786
+ }, [selectedBoard, boardFilter, commentsByIssueId]);
41984
42787
  const attachmentsByIssueId = state?.attachmentsByIssueId ?? {};
41985
42788
  const [attachmentUploadStatus, setAttachmentUploadStatus] = (0, import_react16.useState)({ state: "idle" });
42789
+ (0, import_react16.useEffect)(() => {
42790
+ if (!selectedIssue) {
42791
+ setIssueDetail(null);
42792
+ return;
42793
+ }
42794
+ sendSocketJson(socketRef.current, {
42795
+ type: "issue:get",
42796
+ payload: { issueId: selectedIssue.id }
42797
+ });
42798
+ }, [selectedIssue?.id, state]);
41986
42799
  const requestState = () => {
41987
42800
  sendSocketJson(socketRef.current, { type: "state:get" });
41988
42801
  };
@@ -42010,6 +42823,10 @@ var App = () => {
42010
42823
  const nextState = getResultValue(message.payload);
42011
42824
  if (nextState) setState(nextState);
42012
42825
  }
42826
+ if (message.type === "issue") {
42827
+ const detail = getResultValue(message.payload);
42828
+ if (detail) setIssueDetail(detail);
42829
+ }
42013
42830
  if (message.type === "issue:created") {
42014
42831
  const created = getResultValue(message.payload);
42015
42832
  if (created && boardId) {
@@ -42070,8 +42887,9 @@ var App = () => {
42070
42887
  };
42071
42888
  }, [boardId, navigate]);
42072
42889
  (0, import_react16.useEffect)(() => {
42073
- if (!boardId && state?.boards[0]) {
42074
- void navigate(`/board/${state.boards[0].ref}`, { replace: true });
42890
+ const first = state?.boards?.[0];
42891
+ if (!boardId && first) {
42892
+ void navigate(`/board/${first.ref}`, { replace: true });
42075
42893
  }
42076
42894
  }, [boardId, state, navigate]);
42077
42895
  (0, import_react16.useEffect)(() => {
@@ -42421,7 +43239,7 @@ var App = () => {
42421
43239
  });
42422
43240
  send("issue:comment:delete", { commentId });
42423
43241
  };
42424
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
43242
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
42425
43243
  "div",
42426
43244
  {
42427
43245
  style: {
@@ -42433,30 +43251,38 @@ var App = () => {
42433
43251
  flexDirection: "column"
42434
43252
  },
42435
43253
  children: [
42436
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(GlobalScrollbarStyles, {}),
42437
- commitInspectError && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43254
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(GlobalScrollbarStyles, {}),
43255
+ commitInspectError && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
42438
43256
  ErrorToast,
42439
43257
  {
42440
43258
  message: `Couldn't open commit diff: ${commitInspectError}`,
42441
43259
  onDismiss: () => setCommitInspectError(null)
42442
43260
  }
42443
43261
  ),
42444
- removeError && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43262
+ removeError && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
42445
43263
  ErrorToast,
42446
43264
  {
42447
43265
  message: removeError,
42448
43266
  onDismiss: () => setRemoveError(null)
42449
43267
  }
42450
43268
  ),
42451
- actionError && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43269
+ actionError && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
42452
43270
  ErrorToast,
42453
43271
  {
42454
43272
  message: actionError,
42455
43273
  onDismiss: () => setActionError(null)
42456
43274
  }
42457
43275
  ),
42458
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Header, { state, connected, syncStatus }),
42459
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43276
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
43277
+ Header,
43278
+ {
43279
+ state,
43280
+ connected,
43281
+ scrubbing: state?.timeTravel?.mode === "scrub",
43282
+ syncStatus
43283
+ }
43284
+ ),
43285
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
42460
43286
  TimeScrubber,
42461
43287
  {
42462
43288
  timeline: history.timeline,
@@ -42468,10 +43294,11 @@ var App = () => {
42468
43294
  onInspectCommit: inspectCommit,
42469
43295
  timeTravel: state?.timeTravel ?? { mode: "live", asOfTime: null },
42470
43296
  onScrub: scrubToTime,
42471
- onReturnToLive: returnToLive
43297
+ onReturnToLive: returnToLive,
43298
+ onBoardFilterChange: setBoardFilter
42472
43299
  }
42473
43300
  ),
42474
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
43301
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
42475
43302
  "div",
42476
43303
  {
42477
43304
  style: {
@@ -42480,7 +43307,7 @@ var App = () => {
42480
43307
  overflow: "hidden"
42481
43308
  },
42482
43309
  children: [
42483
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
43310
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
42484
43311
  "main",
42485
43312
  {
42486
43313
  onClick: clearPicked,
@@ -42495,24 +43322,35 @@ var App = () => {
42495
43322
  overflow: "hidden"
42496
43323
  },
42497
43324
  children: [
42498
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { style: { padding: "20px 10px" }, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42499
- Dropdown,
43325
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
43326
+ "div",
42500
43327
  {
42501
- testId: "board-switcher",
42502
- label: "Board:",
42503
- value: selectedBoard ? {
42504
- id: selectedBoard.id,
42505
- label: selectedBoard.title
42506
- } : null,
42507
- items: state?.boards.map((board) => ({
42508
- id: board.id,
42509
- label: board.title
42510
- })) ?? [],
42511
- placeholder: "Loading...",
42512
- onSelect: selectBoard
43328
+ style: {
43329
+ padding: "20px 10px",
43330
+ display: "flex",
43331
+ alignItems: "center",
43332
+ gap: 10
43333
+ },
43334
+ children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
43335
+ Dropdown,
43336
+ {
43337
+ testId: "board-switcher",
43338
+ label: "Board:",
43339
+ value: selectedBoard ? {
43340
+ id: selectedBoard.id,
43341
+ label: selectedBoard.title
43342
+ } : null,
43343
+ items: state?.boards.map((board) => ({
43344
+ id: board.id,
43345
+ label: board.title
43346
+ })) ?? [],
43347
+ placeholder: "Loading...",
43348
+ onSelect: selectBoard
43349
+ }
43350
+ )
42513
43351
  }
42514
- ) }),
42515
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
43352
+ ),
43353
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
42516
43354
  "div",
42517
43355
  {
42518
43356
  style: {
@@ -42524,7 +43362,7 @@ var App = () => {
42524
43362
  overflowY: "hidden"
42525
43363
  },
42526
43364
  children: [
42527
- selectedBoard?.swimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43365
+ visibleSwimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
42528
43366
  SwimlaneColumn,
42529
43367
  {
42530
43368
  swimlane,
@@ -42552,14 +43390,14 @@ var App = () => {
42552
43390
  },
42553
43391
  swimlane.id
42554
43392
  )),
42555
- !(selectedIssue && state?.user) && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { style: { width: ASIDE_WIDTH, flexShrink: 0 } })
43393
+ !(selectedIssue && state?.user) && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { style: { width: ASIDE_WIDTH, flexShrink: 0 } })
42556
43394
  ]
42557
43395
  }
42558
43396
  )
42559
43397
  ]
42560
43398
  }
42561
43399
  ),
42562
- pickedIssues.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43400
+ pickedIssues.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
42563
43401
  BulkDetails,
42564
43402
  {
42565
43403
  issues: pickedIssues,
@@ -42587,13 +43425,13 @@ var App = () => {
42587
43425
  onClear: clearPicked
42588
43426
  }
42589
43427
  ),
42590
- pickedIssues.length <= 1 && selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43428
+ pickedIssues.length <= 1 && selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
42591
43429
  IssueDetails,
42592
43430
  {
42593
43431
  whoAmI: state.user,
42594
- issue: selectedIssue,
43432
+ issue: issueDetail?.issueId === selectedIssue.id ? { ...selectedIssue, description: issueDetail.description } : selectedIssue,
42595
43433
  activeTab: selectedTab,
42596
- comments: commentsByIssueId[selectedIssue.id] ?? [],
43434
+ comments: issueDetail?.issueId === selectedIssue.id ? issueDetail.comments : [],
42597
43435
  onChangeTab: changeIssueDetailsTab,
42598
43436
  onClose: closeIssueDetails,
42599
43437
  onEditTitle: editIssueTitle,
@@ -42620,7 +43458,7 @@ var App = () => {
42620
43458
  ]
42621
43459
  }
42622
43460
  ),
42623
- createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43461
+ createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
42624
43462
  CreateIssueModal,
42625
43463
  {
42626
43464
  title: createIssueModal.title,
@@ -42635,12 +43473,12 @@ var App = () => {
42635
43473
  };
42636
43474
 
42637
43475
  // source/gui/client/main.tsx
42638
- var import_jsx_runtime34 = __toESM(require_jsx_runtime(), 1);
43476
+ var import_jsx_runtime36 = __toESM(require_jsx_runtime(), 1);
42639
43477
  (0, import_client.createRoot)(document.getElementById("root")).render(
42640
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(BrowserRouter, { children: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(Routes, { children: [
42641
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Route, { path: "/", element: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(App, {}) }),
42642
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Route, { path: "/board/:boardId/*", element: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(App, {}) }),
42643
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Route, { path: "*", element: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Navigate, { to: "/", replace: true }) })
43478
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(BrowserRouter, { children: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(Routes, { children: [
43479
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Route, { path: "/", element: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(App, {}) }),
43480
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Route, { path: "/board/:boardId/*", element: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(App, {}) }),
43481
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Route, { path: "*", element: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Navigate, { to: "/", replace: true }) })
42644
43482
  ] }) })
42645
43483
  );
42646
43484
  /*! Bundled license information: