epiq 1.4.2 → 1.4.3

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
@@ -24860,6 +24860,12 @@ var GUI_THEME = {
24860
24860
  green: "#8ce99a",
24861
24861
  red: "#ff8787"
24862
24862
  };
24863
+ var EVENT_CATEGORY_COLORS = {
24864
+ tickets: GUI_THEME.accent,
24865
+ comments: "#ffd479",
24866
+ tagging: "#c9a5ff",
24867
+ assigning: "#ff9ecd"
24868
+ };
24863
24869
 
24864
24870
  // source/gui/client/components/Aside.tsx
24865
24871
  var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
@@ -25318,7 +25324,7 @@ var User = ({
25318
25324
  );
25319
25325
 
25320
25326
  // source/version.ts
25321
- var EPIQ_VERSION = "1.4.2";
25327
+ var EPIQ_VERSION = "1.4.3";
25322
25328
 
25323
25329
  // source/gui/client/components/Header.tsx
25324
25330
  var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
@@ -39897,7 +39903,15 @@ var TicketCard = ({
39897
39903
  paddingTop: 2,
39898
39904
  marginTop: "-4px"
39899
39905
  },
39900
- children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(User, { user: assignee, index: idx, isFocus: isSelected }))
39906
+ children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39907
+ User,
39908
+ {
39909
+ user: assignee,
39910
+ index: idx,
39911
+ isFocus: isSelected
39912
+ },
39913
+ assignee.id
39914
+ ))
39901
39915
  }
39902
39916
  )
39903
39917
  ]
@@ -40203,7 +40217,7 @@ var EVENTS_MODE_TOTAL_HEIGHT = 8 + TRACK_HEIGHT * 2;
40203
40217
  var EVENTS_SCATTER_HEIGHT = TRACK_HEIGHT + 16;
40204
40218
  var EVENTS_MODE_VERTICAL_PADDING = (EVENTS_MODE_TOTAL_HEIGHT - EVENTS_SCATTER_HEIGHT) / 2;
40205
40219
  var HOVER_HINT_WIDTH = 220;
40206
- var SEGMENT_HIGHLIGHT_COLOR = "rgba(122, 158, 214, 0.07)";
40220
+ var SEGMENT_HIGHLIGHT_COLOR = "rgba(122, 157, 214, 0.14)";
40207
40221
  var BUCKET_HIGHLIGHT_COLOR = "rgba(255, 255, 255, 0.06)";
40208
40222
  var NEEDLE_COLOR = "rgba(255, 255, 255, 0.62)";
40209
40223
  var MIN_BUCKET_COUNT_FOR_GAP = 300;
@@ -40239,8 +40253,15 @@ var buildAxis = (timeline, commits, now = Date.now()) => {
40239
40253
  fractionToTime: (fraction) => Math.round(earliest + clamp(fraction, 0, 1) * span)
40240
40254
  };
40241
40255
  };
40242
- var bucketIssueCounts = (axis, timeline) => {
40256
+ var bucketIssueCounts = (axis, timeline, view = "all", hiddenIds = /* @__PURE__ */ new Set()) => {
40243
40257
  const counts = new Array(axis.bucketCount).fill(0);
40258
+ if (timeline && timeline.events.length > 0) {
40259
+ for (const entry of timeline.events) {
40260
+ if (!isShown(entry, view, hiddenIds)) continue;
40261
+ counts[axis.bucketIndexForTime(entry.t)] += 1;
40262
+ }
40263
+ return counts;
40264
+ }
40244
40265
  for (const sparseBucket of timeline?.buckets ?? []) {
40245
40266
  counts[axis.bucketIndexForTime(sparseBucket.t)] += sparseBucket.count;
40246
40267
  }
@@ -40258,6 +40279,102 @@ var bucketCommitStats = (axis, commits) => {
40258
40279
  }
40259
40280
  return byIndex;
40260
40281
  };
40282
+ var EVENT_CATEGORIES = [
40283
+ "tickets",
40284
+ "comments",
40285
+ "tagging",
40286
+ "assigning"
40287
+ ];
40288
+ var BOARD_VIEWS = ["all", ...EVENT_CATEGORIES];
40289
+ var isBoardView = (value) => BOARD_VIEWS.includes(value);
40290
+ var boardViewColor = (view) => view === "all" ? GUI_THEME.accent : EVENT_CATEGORY_COLORS[view];
40291
+ var identityAxisFor = (view) => view === "comments" ? "actor" : view === "tagging" ? "tag" : view === "assigning" ? "assignee" : null;
40292
+ var CATEGORY_BY_ACTION = {
40293
+ "add.issue.comment": "comments",
40294
+ "edit.issue.comment": "comments",
40295
+ "delete.issue.comment": "comments",
40296
+ "add.issue.tag": "tagging",
40297
+ "remove.issue.tag": "tagging",
40298
+ "create.tag": "tagging",
40299
+ "add.issue.assignee": "assigning",
40300
+ "remove.issue.assignee": "assigning",
40301
+ "create.contributor": "assigning",
40302
+ "tombstone.contributor": "assigning",
40303
+ "restore.contributor": "assigning",
40304
+ "link.contributor.user": "assigning"
40305
+ };
40306
+ var categoryOf = (action) => CATEGORY_BY_ACTION[action] ?? "tickets";
40307
+ var identityOf = (entry, view) => {
40308
+ const axis = identityAxisFor(view);
40309
+ return axis === null ? null : entry[axis];
40310
+ };
40311
+ var listIdentities = (timeline, view) => {
40312
+ if (!timeline || identityAxisFor(view) === null) return [];
40313
+ const byId = /* @__PURE__ */ new Map();
40314
+ for (const entry of timeline.events) {
40315
+ if (categoryOf(entry.action) !== view) continue;
40316
+ const identity = identityOf(entry, view);
40317
+ if (identity && !byId.has(identity.id)) byId.set(identity.id, identity);
40318
+ }
40319
+ return [...byId.values()];
40320
+ };
40321
+ var soleVisibleIdentity = (identities, hiddenIds) => {
40322
+ const visible = identities.filter((identity) => !hiddenIds.has(identity.id));
40323
+ return visible.length === 1 ? visible[0] : null;
40324
+ };
40325
+ var dotDetail = (dot) => {
40326
+ const base = dot.label ?? `${dot.count ?? 0} board event${dot.count === 1 ? "" : "s"}`;
40327
+ return dot.identity && !base.includes(dot.identity.name) ? `${base} \u2014 ${dot.identity.name}` : base;
40328
+ };
40329
+ var EVENT_DOT_SIZE = 4;
40330
+ var EVENT_DOT_OPACITY = 0.55;
40331
+ var isShown = (entry, view, hiddenIds) => {
40332
+ if (view !== "all" && categoryOf(entry.action) !== view) return false;
40333
+ const identity = identityOf(entry, view);
40334
+ return identity === null || !hiddenIds.has(identity.id);
40335
+ };
40336
+ var buildEventDots = (timeline, view = "all", hiddenIds = /* @__PURE__ */ new Set()) => {
40337
+ if (!timeline) return [];
40338
+ if (timeline.events.length > 0) {
40339
+ return timeline.events.flatMap((entry, index2) => {
40340
+ if (!isShown(entry, view, hiddenIds)) return [];
40341
+ const category = categoryOf(entry.action);
40342
+ const identity = identityOf(entry, view);
40343
+ return [
40344
+ {
40345
+ // Two events can share a millisecond, so time alone is not a key.
40346
+ key: `${entry.t}-${index2}`,
40347
+ t: entry.t,
40348
+ count: null,
40349
+ label: entry.label,
40350
+ category,
40351
+ color: view === "all" ? EVENT_CATEGORY_COLORS[category] : identity?.color ?? GUI_THEME.accent,
40352
+ identity,
40353
+ size: EVENT_DOT_SIZE,
40354
+ opacity: EVENT_DOT_OPACITY
40355
+ }
40356
+ ];
40357
+ });
40358
+ }
40359
+ const maxCount = maxOf(
40360
+ timeline.buckets.map((bucket) => bucket.count),
40361
+ 1
40362
+ );
40363
+ return timeline.buckets.map((bucket) => {
40364
+ const intensity = bucket.count / maxCount;
40365
+ return {
40366
+ key: String(bucket.t),
40367
+ t: bucket.t,
40368
+ count: bucket.count,
40369
+ label: null,
40370
+ category: null,
40371
+ color: GUI_THEME.accent,
40372
+ identity: null,
40373
+ size: 3 + intensity * 6,
40374
+ opacity: 0.3 + intensity * 0.5
40375
+ };
40376
+ });
40377
+ };
40261
40378
  var populatedRange = (bars) => bars.length > 0 ? [
40262
40379
  bars.reduce((min, bar) => Math.min(min, bar.index), Infinity),
40263
40380
  bars.reduce((max, bar) => Math.max(max, bar.index), -Infinity)
@@ -40266,8 +40383,9 @@ var hourFractionForTime = (time) => {
40266
40383
  const date = new Date(time);
40267
40384
  return (date.getHours() * 60 + date.getMinutes()) / (24 * 60);
40268
40385
  };
40269
- var SEGMENT_UNIT_ORDER = ["day", "week", "month", "year"];
40386
+ var SEGMENT_UNIT_ORDER = ["hour", "day", "week", "month", "year"];
40270
40387
  var APPROX_UNIT_MS = {
40388
+ hour: 60 * 60 * 1e3,
40271
40389
  day: 24 * 60 * 60 * 1e3,
40272
40390
  week: 7 * 24 * 60 * 60 * 1e3,
40273
40391
  month: 30.44 * 24 * 60 * 60 * 1e3,
@@ -40293,14 +40411,16 @@ var MONTH_LABELS = [
40293
40411
  "Dec"
40294
40412
  ];
40295
40413
  var advanceByUnit = (date, unit) => {
40296
- if (unit === "day") date.setDate(date.getDate() + 1);
40414
+ if (unit === "hour") date.setHours(date.getHours() + 1);
40415
+ else if (unit === "day") date.setDate(date.getDate() + 1);
40297
40416
  else if (unit === "week") date.setDate(date.getDate() + 7);
40298
40417
  else if (unit === "month") date.setMonth(date.getMonth() + 1);
40299
40418
  else date.setFullYear(date.getFullYear() + 1);
40300
40419
  };
40301
40420
  var segmentAt = (time, unit) => {
40302
40421
  const start2 = new Date(time);
40303
- start2.setHours(0, 0, 0, 0);
40422
+ if (unit === "hour") start2.setMinutes(0, 0, 0);
40423
+ else start2.setHours(0, 0, 0, 0);
40304
40424
  if (unit === "week") {
40305
40425
  start2.setDate(start2.getDate() - (start2.getDay() + 6) % 7);
40306
40426
  } else if (unit === "month") {
@@ -40311,7 +40431,10 @@ var segmentAt = (time, unit) => {
40311
40431
  const end = new Date(start2);
40312
40432
  advanceByUnit(end, unit);
40313
40433
  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()}`;
40434
+ const label = unit === "hour" ? `${WEEKDAY_LABELS[start2.getDay()]} ${String(start2.getHours()).padStart(
40435
+ 2,
40436
+ "0"
40437
+ )}: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
40438
  return { start: start2.getTime(), end: end.getTime(), label };
40316
40439
  };
40317
40440
  var formatInterval = (start2, end) => {
@@ -40320,12 +40443,19 @@ var formatInterval = (start2, end) => {
40320
40443
  const endLabel = isSameDay(startDate, endDate) ? formatTimeOfDay(endDate) : formatDateTime(endDate);
40321
40444
  return `${formatDateTime(startDate)} \u2013 ${endLabel}`;
40322
40445
  };
40323
- var SCOPES = ["week", "month", "year", "all"];
40446
+ var SCOPES = ["day", "week", "month", "year", "all"];
40324
40447
  var SCOPE_DURATION_MS = {
40448
+ day: 24 * 60 * 60 * 1e3,
40325
40449
  week: 7 * 24 * 60 * 60 * 1e3,
40326
40450
  month: 30 * 24 * 60 * 60 * 1e3,
40327
40451
  year: 365 * 24 * 60 * 60 * 1e3
40328
40452
  };
40453
+ var SCOPE_RECENT_LABELS = {
40454
+ day: "Last 24 hours",
40455
+ week: "Last 7 days",
40456
+ month: "Last 30 days",
40457
+ year: "Last 365 days"
40458
+ };
40329
40459
  var isScope = (value) => SCOPES.includes(value ?? "");
40330
40460
  var getPeriodRange = (scope, offset) => {
40331
40461
  if (scope === "all") return null;
@@ -40335,9 +40465,7 @@ var getPeriodRange = (scope, offset) => {
40335
40465
  };
40336
40466
  var formatPeriodLabel = (scope, offset, range) => {
40337
40467
  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
- }
40468
+ if (offset === 0) return SCOPE_RECENT_LABELS[scope];
40341
40469
  const start2 = new Date(range.start);
40342
40470
  const end = new Date(range.end);
40343
40471
  return `${start2.getMonth() + 1}/${start2.getDate()} \u2013 ${end.getMonth() + 1}/${end.getDate()}`;
@@ -40354,10 +40482,16 @@ var hashUnitInterval = (key) => {
40354
40482
  var DOT_APPEAR_MS = 260;
40355
40483
  var DOT_APPEAR_SCATTER_MS = 620;
40356
40484
  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`;
40485
+ var easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);
40486
+ var easeInCubic = (t) => t * t * t;
40487
+ var dotEntranceScale = (key, elapsedMs) => easeOutCubic(clamp((elapsedMs - dotDelayMs(key)) / DOT_APPEAR_MS, 0, 1));
40488
+ var dotExitScale = (key, elapsedMs) => 1 - easeInCubic(
40489
+ clamp(
40490
+ (elapsedMs - (DOT_APPEAR_SCATTER_MS - dotDelayMs(key))) / DOT_APPEAR_MS,
40491
+ 0,
40492
+ 1
40493
+ )
40494
+ );
40361
40495
  var DOT_EXIT_TOTAL_MS = DOT_APPEAR_SCATTER_MS + DOT_APPEAR_MS;
40362
40496
  var BAR_GROW_MS = 200;
40363
40497
  var BAR_GROW_SWEEP_MS = 560;
@@ -40450,6 +40584,19 @@ var usePersistedFlag = (key, fallback) => {
40450
40584
  }
40451
40585
  ];
40452
40586
  };
40587
+ var buildBoardFilter = (view, identities, hiddenIds) => {
40588
+ const axis = identityAxisFor(view);
40589
+ if (axis === null || hiddenIds.size === 0) return null;
40590
+ const visibleIds = new Set(
40591
+ identities.map((identity) => identity.id).filter((id) => !hiddenIds.has(id))
40592
+ );
40593
+ return { axis, visibleIds };
40594
+ };
40595
+ var issuePassesBoardFilter = (issue, commentAuthorIds, filter) => {
40596
+ if (!filter) return true;
40597
+ const ids = filter.axis === "tag" ? issue.tags.map((tag) => tag.id) : filter.axis === "assignee" ? issue.assignees.map((assignee) => assignee.id) : commentAuthorIds;
40598
+ return ids.some((id) => filter.visibleIds.has(id));
40599
+ };
40453
40600
 
40454
40601
  // source/gui/client/components/ScrubberParts.tsx
40455
40602
  var import_react14 = __toESM(require_react(), 1);
@@ -40581,6 +40728,319 @@ var toggleButtonStyle = (active) => ({
40581
40728
  padding: "2px 8px",
40582
40729
  cursor: "pointer"
40583
40730
  });
40731
+ var CATEGORY_LABELS = {
40732
+ tickets: "Tickets",
40733
+ comments: "Comments",
40734
+ tagging: "Tags",
40735
+ assigning: "Assignees"
40736
+ };
40737
+ var disclosureStyle = {
40738
+ background: "transparent",
40739
+ border: "none",
40740
+ padding: 0,
40741
+ display: "inline-flex",
40742
+ alignItems: "center",
40743
+ color: GUI_THEME.secondary,
40744
+ cursor: "pointer"
40745
+ };
40746
+ var VIEW_LABELS = {
40747
+ all: "All board events",
40748
+ ...CATEGORY_LABELS
40749
+ };
40750
+ var SELECT_TRIGGER_WIDTH = 180;
40751
+ var selectTriggerStyle = (color2, disabled) => ({
40752
+ display: "inline-flex",
40753
+ alignItems: "center",
40754
+ justifyContent: "space-between",
40755
+ gap: 6,
40756
+ width: SELECT_TRIGGER_WIDTH,
40757
+ boxSizing: "border-box",
40758
+ background: "rgba(208, 223, 255, 0.08)",
40759
+ // None, but padded as though there were, so it sits at the same height as
40760
+ // the bordered toggles on either side.
40761
+ border: "none",
40762
+ color: disabled ? GUI_THEME.dim : color2,
40763
+ borderRadius: 6,
40764
+ fontFamily: "inherit",
40765
+ fontSize: 10,
40766
+ padding: "3px 7px 3px 9px",
40767
+ cursor: disabled ? "not-allowed" : "pointer",
40768
+ opacity: disabled ? 0.4 : 1
40769
+ });
40770
+ var nestedListStyle = {
40771
+ display: "flex",
40772
+ flexDirection: "column",
40773
+ gap: 7,
40774
+ marginLeft: 5,
40775
+ paddingLeft: 8,
40776
+ borderLeft: `1px solid ${GUI_THEME.line}`
40777
+ };
40778
+ var popoverStyle = {
40779
+ position: "absolute",
40780
+ top: "100%",
40781
+ left: 0,
40782
+ marginTop: 6,
40783
+ // Carries the column and its gap itself. Without them the options stack as
40784
+ // plain blocks and their radios sit edge to edge.
40785
+ display: "flex",
40786
+ flexDirection: "column",
40787
+ gap: 7,
40788
+ padding: "10px 14px 10px 10px",
40789
+ // Sized for the common case up front, so opening a kind with a list does
40790
+ // not visibly widen the panel under the pointer.
40791
+ minWidth: 200,
40792
+ background: GUI_THEME.panel2,
40793
+ border: `1px solid ${GUI_THEME.line}`,
40794
+ borderRadius: 8,
40795
+ boxShadow: "0 8px 24px rgba(0, 0, 0, 0.45)",
40796
+ // Above the dots and the needle, which sit at 1 and 2.
40797
+ zIndex: 30,
40798
+ whiteSpace: "nowrap"
40799
+ };
40800
+ var onlyButtonStyle = {
40801
+ background: "transparent",
40802
+ border: `1px solid ${GUI_THEME.line}`,
40803
+ borderRadius: 4,
40804
+ color: GUI_THEME.dim,
40805
+ fontFamily: "inherit",
40806
+ fontSize: 9,
40807
+ lineHeight: 1,
40808
+ padding: "2px 4px",
40809
+ cursor: "pointer"
40810
+ };
40811
+ var useDismissOnOutsideClick = (open, onDismiss) => {
40812
+ const ref = (0, import_react14.useRef)(null);
40813
+ (0, import_react14.useEffect)(() => {
40814
+ if (!open) return;
40815
+ const onPointerDown = (event) => {
40816
+ if (!ref.current?.contains(event.target)) onDismiss();
40817
+ };
40818
+ const onKeyDown = (event) => {
40819
+ if (event.key === "Escape") onDismiss();
40820
+ };
40821
+ document.addEventListener("mousedown", onPointerDown);
40822
+ document.addEventListener("keydown", onKeyDown);
40823
+ return () => {
40824
+ document.removeEventListener("mousedown", onPointerDown);
40825
+ document.removeEventListener("keydown", onKeyDown);
40826
+ };
40827
+ }, [open, onDismiss]);
40828
+ return ref;
40829
+ };
40830
+ var Radio = ({
40831
+ label,
40832
+ selected,
40833
+ color: color2,
40834
+ disabled,
40835
+ onSelect
40836
+ }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40837
+ "button",
40838
+ {
40839
+ type: "button",
40840
+ role: "radio",
40841
+ "aria-checked": selected,
40842
+ disabled,
40843
+ onClick: onSelect,
40844
+ style: {
40845
+ display: "flex",
40846
+ alignItems: "center",
40847
+ gap: 5,
40848
+ background: "transparent",
40849
+ border: "none",
40850
+ padding: 0,
40851
+ fontSize: 10,
40852
+ fontFamily: "inherit",
40853
+ color: selected ? color2 : GUI_THEME.dim,
40854
+ cursor: disabled ? "not-allowed" : "pointer",
40855
+ opacity: disabled ? 0.4 : 1
40856
+ },
40857
+ children: [
40858
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40859
+ "span",
40860
+ {
40861
+ style: {
40862
+ width: 12,
40863
+ height: 12,
40864
+ borderRadius: "50%",
40865
+ border: `1px solid ${selected ? color2 : GUI_THEME.dim}`,
40866
+ display: "inline-flex",
40867
+ alignItems: "center",
40868
+ justifyContent: "center",
40869
+ flexShrink: 0
40870
+ },
40871
+ children: selected && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40872
+ "span",
40873
+ {
40874
+ style: {
40875
+ width: 6,
40876
+ height: 6,
40877
+ borderRadius: "50%",
40878
+ background: color2
40879
+ }
40880
+ }
40881
+ )
40882
+ }
40883
+ ),
40884
+ label
40885
+ ]
40886
+ }
40887
+ );
40888
+ var BoardSeriesGroup = ({
40889
+ showIssues,
40890
+ view,
40891
+ identities,
40892
+ hiddenIds,
40893
+ expanded,
40894
+ identitiesExpanded,
40895
+ filtered,
40896
+ onChangeShowIssues,
40897
+ onChangeView,
40898
+ onToggleIdentity,
40899
+ onOnlyIdentity,
40900
+ onToggleExpanded,
40901
+ onSetIdentitiesExpanded
40902
+ }) => {
40903
+ const sole = soleVisibleIdentity(identities, hiddenIds);
40904
+ const partial = filtered && hiddenIds.size > 0 && identityAxisFor(view) !== null;
40905
+ const label = sole ? `${VIEW_LABELS[view]}: ${sole.name}` : partial ? `${VIEW_LABELS[view]} (multi)` : VIEW_LABELS[view];
40906
+ const color2 = sole?.color ?? (partial ? GUI_THEME.dim2 : boardViewColor(view));
40907
+ const ref = useDismissOnOutsideClick(expanded, onToggleExpanded);
40908
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40909
+ "div",
40910
+ {
40911
+ ref,
40912
+ style: { position: "relative", display: "flex", flexDirection: "column" },
40913
+ children: [
40914
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
40915
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40916
+ Checkbox,
40917
+ {
40918
+ label: null,
40919
+ title: "Show board events",
40920
+ checked: showIssues,
40921
+ activeColor: color2,
40922
+ onChange: onChangeShowIssues
40923
+ }
40924
+ ),
40925
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40926
+ "button",
40927
+ {
40928
+ type: "button",
40929
+ onClick: onToggleExpanded,
40930
+ disabled: !showIssues,
40931
+ title: filtered ? "Choose what the board series plots" : "This window holds too many events to split by kind",
40932
+ "aria-haspopup": "listbox",
40933
+ "aria-expanded": expanded,
40934
+ style: selectTriggerStyle(color2, !showIssues),
40935
+ children: [
40936
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40937
+ "span",
40938
+ {
40939
+ style: {
40940
+ overflow: "hidden",
40941
+ textOverflow: "ellipsis",
40942
+ whiteSpace: "nowrap"
40943
+ },
40944
+ children: label
40945
+ }
40946
+ ),
40947
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { style: { display: "inline-flex", flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(IconChevronDown, { size: 12 }) })
40948
+ ]
40949
+ }
40950
+ )
40951
+ ] }),
40952
+ expanded && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { role: "radiogroup", style: popoverStyle, children: BOARD_VIEWS.map((option) => {
40953
+ const selected = view === option;
40954
+ const hasList = identityAxisFor(option) !== null;
40955
+ const open = selected && identitiesExpanded;
40956
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40957
+ "div",
40958
+ {
40959
+ style: { display: "flex", flexDirection: "column", gap: 7 },
40960
+ children: [
40961
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 3 }, children: [
40962
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40963
+ Radio,
40964
+ {
40965
+ label: VIEW_LABELS[option],
40966
+ selected,
40967
+ color: boardViewColor(option),
40968
+ disabled: !showIssues || !filtered,
40969
+ onSelect: () => onChangeView(option)
40970
+ }
40971
+ ),
40972
+ hasList && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40973
+ "button",
40974
+ {
40975
+ type: "button",
40976
+ disabled: !showIssues || !filtered,
40977
+ onClick: () => {
40978
+ if (!selected) onChangeView(option);
40979
+ onSetIdentitiesExpanded(!open);
40980
+ },
40981
+ title: open ? "Hide the list" : "Pick which to show",
40982
+ "aria-expanded": open,
40983
+ style: disclosureStyle,
40984
+ children: open ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(IconChevronDown, { size: 12 }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(IconChevronRight, { size: 12 })
40985
+ }
40986
+ )
40987
+ ] }),
40988
+ open && identities.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40989
+ "div",
40990
+ {
40991
+ style: {
40992
+ ...nestedListStyle,
40993
+ // A repo with dozens of tags would otherwise push the
40994
+ // board itself off the screen.
40995
+ maxHeight: 132,
40996
+ overflowY: "auto"
40997
+ },
40998
+ children: identities.map((identity) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40999
+ "div",
41000
+ {
41001
+ style: {
41002
+ display: "flex",
41003
+ alignItems: "center",
41004
+ justifyContent: "space-between",
41005
+ gap: 10
41006
+ },
41007
+ children: [
41008
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41009
+ Checkbox,
41010
+ {
41011
+ label: identity.name,
41012
+ checked: !hiddenIds.has(identity.id),
41013
+ activeColor: identity.color,
41014
+ disabled: !showIssues,
41015
+ onChange: (next) => onToggleIdentity(identity.id, next)
41016
+ }
41017
+ ),
41018
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41019
+ "button",
41020
+ {
41021
+ type: "button",
41022
+ title: `Show only ${identity.name}`,
41023
+ disabled: !showIssues,
41024
+ onClick: () => onOnlyIdentity(identity.id),
41025
+ style: onlyButtonStyle,
41026
+ children: "only"
41027
+ }
41028
+ )
41029
+ ]
41030
+ },
41031
+ identity.id
41032
+ ))
41033
+ }
41034
+ )
41035
+ ]
41036
+ },
41037
+ option
41038
+ );
41039
+ }) })
41040
+ ]
41041
+ }
41042
+ );
41043
+ };
40584
41044
  var navButtonStyle = {
40585
41045
  background: "transparent",
40586
41046
  border: `1px solid ${GUI_THEME.dim}`,
@@ -40599,6 +41059,12 @@ var ScrubberControls = ({
40599
41059
  showIssues,
40600
41060
  showCommits,
40601
41061
  allBoards,
41062
+ boardView,
41063
+ identities,
41064
+ hiddenIdentityIds,
41065
+ categoriesExpanded,
41066
+ identitiesExpanded,
41067
+ categoriesFiltered,
40602
41068
  isScrubbing,
40603
41069
  nowLabel,
40604
41070
  onChangeScope,
@@ -40607,6 +41073,11 @@ var ScrubberControls = ({
40607
41073
  onChangeShowIssues,
40608
41074
  onChangeShowCommits,
40609
41075
  onChangeAllBoards,
41076
+ onChangeBoardView,
41077
+ onToggleIdentity,
41078
+ onOnlyIdentity,
41079
+ onToggleCategoriesExpanded,
41080
+ onSetIdentitiesExpanded,
40610
41081
  onReturnToLive
40611
41082
  }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
40612
41083
  /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
@@ -40682,16 +41153,7 @@ var ScrubberControls = ({
40682
41153
  }
40683
41154
  )
40684
41155
  ] }),
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
- ),
41156
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", gap: 12, alignItems: "center" }, children: [
40695
41157
  /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40696
41158
  Checkbox,
40697
41159
  {
@@ -40702,12 +41164,21 @@ var ScrubberControls = ({
40702
41164
  }
40703
41165
  ),
40704
41166
  /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40705
- Checkbox,
41167
+ BoardSeriesGroup,
40706
41168
  {
40707
- label: "All boards",
40708
- checked: allBoards,
40709
- activeColor: GUI_THEME.accent,
40710
- onChange: onChangeAllBoards
41169
+ showIssues,
41170
+ view: boardView,
41171
+ identities,
41172
+ hiddenIds: hiddenIdentityIds,
41173
+ expanded: categoriesExpanded,
41174
+ identitiesExpanded,
41175
+ filtered: categoriesFiltered,
41176
+ onChangeShowIssues,
41177
+ onChangeView: onChangeBoardView,
41178
+ onToggleIdentity,
41179
+ onOnlyIdentity,
41180
+ onToggleExpanded: onToggleCategoriesExpanded,
41181
+ onSetIdentitiesExpanded
40711
41182
  }
40712
41183
  )
40713
41184
  ] }),
@@ -40881,7 +41352,11 @@ var SeriesLayer = ({
40881
41352
  position: "absolute",
40882
41353
  inset: 0,
40883
41354
  pointerEvents: "none",
40884
- animation: animate ? FADE_IN_ANIMATION : void 0
41355
+ animation: animate ? FADE_IN_ANIMATION : void 0,
41356
+ // Walls this subtree off from the needle and highlight moving above
41357
+ // it: without containment every frame of a drag re-lays-out the
41358
+ // thousands of dots inside, and the drag stutters.
41359
+ contain: "layout paint"
40885
41360
  },
40886
41361
  children
40887
41362
  }
@@ -40942,47 +41417,6 @@ var VolumeBars = ({
40942
41417
  ))
40943
41418
  ] });
40944
41419
  };
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
41420
  var ScrubberNeedle = ({ fraction }) => {
40987
41421
  const [hovered, setHovered] = (0, import_react14.useState)(false);
40988
41422
  const hover = {
@@ -41102,10 +41536,175 @@ var ScrubberHoverHint = ({
41102
41536
  ]
41103
41537
  }
41104
41538
  );
41539
+ var HIT_RADIUS = 7;
41540
+ var pointAt = (layers, x, y, width) => {
41541
+ let best = null;
41542
+ let bestDistance = HIT_RADIUS * HIT_RADIUS;
41543
+ for (const layer of layers) {
41544
+ if (layer.leaving) continue;
41545
+ for (const point4 of layer.points) {
41546
+ const dx = point4.fraction * width - x;
41547
+ const dy = EVENTS_MODE_VERTICAL_PADDING + point4.hourFraction * EVENTS_SCATTER_HEIGHT - y;
41548
+ const distance = dx * dx + dy * dy;
41549
+ if (distance <= bestDistance) {
41550
+ bestDistance = distance;
41551
+ best = point4;
41552
+ }
41553
+ }
41554
+ }
41555
+ return best;
41556
+ };
41557
+ var ScatterCanvas = ({
41558
+ layers,
41559
+ animate,
41560
+ onPointEnter,
41561
+ onPointLeave,
41562
+ onInspectCommit
41563
+ }) => {
41564
+ const canvasRef = (0, import_react14.useRef)(null);
41565
+ const sizeRef = (0, import_react14.useRef)({ width: 0, height: 0 });
41566
+ const layersRef = (0, import_react14.useRef)(layers);
41567
+ layersRef.current = layers;
41568
+ const phasesRef = (0, import_react14.useRef)(/* @__PURE__ */ new Map());
41569
+ const frameRef = (0, import_react14.useRef)(null);
41570
+ const hoveredRef = (0, import_react14.useRef)(null);
41571
+ const [entrancePlaying, setEntrancePlaying] = (0, import_react14.useState)(false);
41572
+ const paint = (0, import_react14.useCallback)((now) => {
41573
+ const canvas = canvasRef.current;
41574
+ const context = canvas?.getContext("2d");
41575
+ if (!canvas || !context) return false;
41576
+ const { width, height } = sizeRef.current;
41577
+ context.clearRect(0, 0, width, height);
41578
+ let running = false;
41579
+ for (const layer of layersRef.current) {
41580
+ const phase = phasesRef.current.get(layer.id);
41581
+ const elapsed = phase ? now - phase.startedAt : null;
41582
+ const done = elapsed === null || elapsed >= DOT_EXIT_TOTAL_MS;
41583
+ if (phase && !done) running = true;
41584
+ if (phase?.mode === "out" && done) continue;
41585
+ for (const point4 of layer.points) {
41586
+ const scale = phase === null || phase === void 0 || done ? 1 : phase.mode === "in" ? dotEntranceScale(point4.key, elapsed) : dotExitScale(point4.key, elapsed);
41587
+ if (scale <= 0) continue;
41588
+ context.globalAlpha = point4.opacity;
41589
+ context.fillStyle = point4.color;
41590
+ context.beginPath();
41591
+ context.arc(
41592
+ point4.fraction * width,
41593
+ EVENTS_MODE_VERTICAL_PADDING + point4.hourFraction * EVENTS_SCATTER_HEIGHT,
41594
+ point4.radius * scale,
41595
+ 0,
41596
+ Math.PI * 2
41597
+ );
41598
+ context.fill();
41599
+ }
41600
+ }
41601
+ context.globalAlpha = 1;
41602
+ return running;
41603
+ }, []);
41604
+ const run = (0, import_react14.useCallback)(() => {
41605
+ if (frameRef.current !== null) return;
41606
+ const step = () => {
41607
+ frameRef.current = null;
41608
+ const running = paint(performance.now());
41609
+ if (running) frameRef.current = requestAnimationFrame(step);
41610
+ else setEntrancePlaying(false);
41611
+ };
41612
+ frameRef.current = requestAnimationFrame(step);
41613
+ }, [paint]);
41614
+ (0, import_react14.useEffect)(() => {
41615
+ const canvas = canvasRef.current;
41616
+ const parent = canvas?.parentElement;
41617
+ if (!canvas || !parent) return;
41618
+ const resize = () => {
41619
+ const ratio = window.devicePixelRatio || 1;
41620
+ const { width, height } = parent.getBoundingClientRect();
41621
+ sizeRef.current = { width, height };
41622
+ canvas.width = Math.round(width * ratio);
41623
+ canvas.height = Math.round(height * ratio);
41624
+ canvas.style.width = `${width}px`;
41625
+ canvas.style.height = `${height}px`;
41626
+ canvas.getContext("2d")?.setTransform(ratio, 0, 0, ratio, 0, 0);
41627
+ paint(performance.now());
41628
+ };
41629
+ resize();
41630
+ const observer = new ResizeObserver(resize);
41631
+ observer.observe(parent);
41632
+ return () => observer.disconnect();
41633
+ }, [paint]);
41634
+ const signature = layers.map((layer) => `${layer.id}:${layer.generation}:${layer.leaving}`).join("|");
41635
+ (0, import_react14.useEffect)(() => {
41636
+ const seen = /* @__PURE__ */ new Set();
41637
+ for (const layer of layers) {
41638
+ seen.add(layer.id);
41639
+ const key = `${layer.generation}:${layer.leaving}`;
41640
+ const marker = `${layer.id}@${key}`;
41641
+ const current = phasesRef.current.get(layer.id);
41642
+ if (current?.marker === marker)
41643
+ continue;
41644
+ if (!animate) {
41645
+ phasesRef.current.delete(layer.id);
41646
+ continue;
41647
+ }
41648
+ phasesRef.current.set(layer.id, {
41649
+ mode: layer.leaving ? "out" : "in",
41650
+ startedAt: performance.now(),
41651
+ marker
41652
+ });
41653
+ if (!layer.leaving) setEntrancePlaying(true);
41654
+ }
41655
+ for (const id of [...phasesRef.current.keys()])
41656
+ if (!seen.has(id)) phasesRef.current.delete(id);
41657
+ if (animate) run();
41658
+ else paint(performance.now());
41659
+ }, [signature, animate, run, paint]);
41660
+ (0, import_react14.useEffect)(() => {
41661
+ if (frameRef.current === null) paint(performance.now());
41662
+ }, [layers, paint]);
41663
+ (0, import_react14.useEffect)(
41664
+ () => () => {
41665
+ if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
41666
+ },
41667
+ []
41668
+ );
41669
+ const hitTest = (event) => {
41670
+ const rect = event.currentTarget.getBoundingClientRect();
41671
+ return pointAt(
41672
+ layersRef.current,
41673
+ event.clientX - rect.left,
41674
+ event.clientY - rect.top,
41675
+ rect.width
41676
+ );
41677
+ };
41678
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41679
+ "canvas",
41680
+ {
41681
+ ref: canvasRef,
41682
+ "data-entrance": entrancePlaying ? "playing" : "done",
41683
+ style: { position: "absolute", inset: 0 },
41684
+ onMouseMove: (event) => {
41685
+ const point4 = hitTest(event);
41686
+ if (point4?.key === hoveredRef.current) return;
41687
+ hoveredRef.current = point4?.key ?? null;
41688
+ if (point4) onPointEnter(point4);
41689
+ else onPointLeave();
41690
+ },
41691
+ onMouseLeave: () => {
41692
+ hoveredRef.current = null;
41693
+ onPointLeave();
41694
+ },
41695
+ onPointerDown: (event) => {
41696
+ if (hitTest(event)?.commitSha) event.stopPropagation();
41697
+ },
41698
+ onClick: (event) => {
41699
+ const sha = hitTest(event)?.commitSha;
41700
+ if (sha) onInspectCommit(sha);
41701
+ }
41702
+ }
41703
+ );
41704
+ };
41105
41705
 
41106
41706
  // source/gui/client/components/ScrubberLayout.tsx
41107
41707
  var import_jsx_runtime31 = __toESM(require_jsx_runtime(), 1);
41108
- var dotAnimation = (key, animate, leaving) => !animate ? void 0 : leaving ? dotExitAnimation(key) : dotAppearAnimation(key);
41109
41708
  var ScrubberLayout = ({
41110
41709
  collapsed,
41111
41710
  onToggleCollapsed,
@@ -41211,7 +41810,7 @@ var ScrubberLayout = ({
41211
41810
  /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41212
41811
  TrackBaseline,
41213
41812
  {
41214
- color: GUI_THEME.accent,
41813
+ color: chart.issueSeriesColor,
41215
41814
  anchor: layoutMode === "even" ? "bottom" : "centre"
41216
41815
  }
41217
41816
  ),
@@ -41224,70 +41823,20 @@ var ScrubberLayout = ({
41224
41823
  firstBar: chart.issueBarRange[0],
41225
41824
  lastBar: chart.issueBarRange[1],
41226
41825
  highlightedIndex: chart.hoveredBucketIndex,
41227
- color: GUI_THEME.accent,
41826
+ color: chart.issueSeriesColor,
41228
41827
  direction: "up",
41229
41828
  animate
41230
41829
  }
41231
41830
  ) }, `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,
41831
+ layoutMode === "real" && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41832
+ ScatterCanvas,
41265
41833
  {
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}`
41834
+ layers: chart.scatterLayers,
41835
+ animate,
41836
+ onPointEnter: on.onScatterPointEnter,
41837
+ onPointLeave: on.onScatterPointLeave,
41838
+ onInspectCommit: on.onInspectCommit
41839
+ }
41291
41840
  )
41292
41841
  ]
41293
41842
  }
@@ -41329,7 +41878,7 @@ var ScrubberLayout = ({
41329
41878
  {
41330
41879
  ...chart.boardHint,
41331
41880
  segmentLabel: chart.hoveredSegment?.label,
41332
- stripeColor: GUI_THEME.accent,
41881
+ stripeColor: chart.issueSeriesColor,
41333
41882
  trackWidthPx: chart.trackWidthPx
41334
41883
  }
41335
41884
  ),
@@ -41356,11 +41905,29 @@ var ScrubberLayout = ({
41356
41905
  // source/gui/client/components/TimeScrubber.tsx
41357
41906
  var import_jsx_runtime32 = __toESM(require_jsx_runtime(), 1);
41358
41907
  var SCRUB_THROTTLE_MS = 120;
41908
+ var boardEventRow = (count) => `${count} board event${count === 1 ? "" : "s"}`;
41359
41909
  var COLLAPSED_STORAGE_KEY = "epiq.timeScrubber.collapsed";
41360
41910
  var SCOPE_STORAGE_KEY = "epiq.timeScrubber.scope";
41361
41911
  var SHOW_ISSUES_STORAGE_KEY = "epiq.timeScrubber.showIssues";
41362
41912
  var SHOW_COMMITS_STORAGE_KEY = "epiq.timeScrubber.showCommits";
41363
41913
  var ALL_BOARDS_STORAGE_KEY = "epiq.timeScrubber.allBoards";
41914
+ var BOARD_VIEW_STORAGE_KEY = "epiq.timeScrubber.boardView";
41915
+ var HIDDEN_IDENTITIES_STORAGE_KEY = "epiq.timeScrubber.hiddenIdentities";
41916
+ var CATEGORIES_EXPANDED_STORAGE_KEY = "epiq.timeScrubber.categoriesExpanded";
41917
+ var IDENTITIES_EXPANDED_STORAGE_KEY = "epiq.timeScrubber.identitiesExpanded";
41918
+ var readStoredBoardView = () => {
41919
+ const stored = localStorage.getItem(BOARD_VIEW_STORAGE_KEY);
41920
+ return isBoardView(stored) ? stored : "all";
41921
+ };
41922
+ var readStoredHiddenIds = () => {
41923
+ try {
41924
+ const stored = localStorage.getItem(HIDDEN_IDENTITIES_STORAGE_KEY);
41925
+ const parsed = stored ? JSON.parse(stored) : [];
41926
+ return new Set(Array.isArray(parsed) ? parsed.map(String) : []);
41927
+ } catch {
41928
+ return /* @__PURE__ */ new Set();
41929
+ }
41930
+ };
41364
41931
  var readStoredScope = () => {
41365
41932
  const stored = localStorage.getItem(SCOPE_STORAGE_KEY);
41366
41933
  return isScope(stored) ? stored : "all";
@@ -41375,7 +41942,8 @@ var TimeScrubber = ({
41375
41942
  onRequestHistory,
41376
41943
  boardId,
41377
41944
  connected,
41378
- onInspectCommit
41945
+ onInspectCommit,
41946
+ onBoardFilterChange
41379
41947
  }) => {
41380
41948
  const animate = !usePrefersReducedMotion();
41381
41949
  const trackRef = (0, import_react15.useRef)(null);
@@ -41396,6 +41964,16 @@ var TimeScrubber = ({
41396
41964
  SHOW_COMMITS_STORAGE_KEY,
41397
41965
  true
41398
41966
  );
41967
+ const [boardView, setBoardView] = (0, import_react15.useState)(readStoredBoardView);
41968
+ const [hiddenIdentityIds, setHiddenIdentityIds] = (0, import_react15.useState)(readStoredHiddenIds);
41969
+ const [categoriesExpanded, setCategoriesExpanded] = usePersistedFlag(
41970
+ CATEGORIES_EXPANDED_STORAGE_KEY,
41971
+ false
41972
+ );
41973
+ const [identitiesExpanded, setIdentitiesExpanded] = usePersistedFlag(
41974
+ IDENTITIES_EXPANDED_STORAGE_KEY,
41975
+ false
41976
+ );
41399
41977
  const [allBoards, setAllBoards] = usePersistedFlag(
41400
41978
  ALL_BOARDS_STORAGE_KEY,
41401
41979
  false
@@ -41425,19 +42003,124 @@ var TimeScrubber = ({
41425
42003
  armEntrance();
41426
42004
  setOffset(nextOffset);
41427
42005
  };
42006
+ const changeBoardView = (next) => {
42007
+ setBoardView(next);
42008
+ localStorage.setItem(BOARD_VIEW_STORAGE_KEY, next);
42009
+ };
42010
+ const isolateIdentity = (id) => {
42011
+ const others = identities.filter((identity) => identity.id !== id).map((identity) => identity.id);
42012
+ setHiddenIdentityIds((previous3) => {
42013
+ const isolated = !previous3.has(id) && others.length === previous3.size && others.every((other) => previous3.has(other));
42014
+ const hidden = isolated ? /* @__PURE__ */ new Set() : new Set(others);
42015
+ localStorage.setItem(
42016
+ HIDDEN_IDENTITIES_STORAGE_KEY,
42017
+ JSON.stringify([...hidden])
42018
+ );
42019
+ return hidden;
42020
+ });
42021
+ };
42022
+ const toggleIdentity = (id, next) => {
42023
+ setHiddenIdentityIds((previous3) => {
42024
+ const hidden = new Set(previous3);
42025
+ if (next) {
42026
+ hidden.delete(id);
42027
+ } else {
42028
+ hidden.add(id);
42029
+ }
42030
+ localStorage.setItem(
42031
+ HIDDEN_IDENTITIES_STORAGE_KEY,
42032
+ JSON.stringify([...hidden])
42033
+ );
42034
+ return hidden;
42035
+ });
42036
+ };
41428
42037
  const changeAllBoards = (next) => {
41429
42038
  armEntrance();
41430
42039
  setAllBoards(next);
41431
42040
  };
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
42041
+ const steadyBoardId = (0, import_react15.useRef)(boardId);
42042
+ if (boardId !== null) steadyBoardId.current = boardId;
42043
+ const requestKey = (0, import_react15.useMemo)(
42044
+ () => JSON.stringify([scope, offset, steadyBoardId.current, allBoards]),
42045
+ [scope, offset, steadyBoardId.current, allBoards]
42046
+ );
42047
+ const filterKey = (0, import_react15.useMemo)(
42048
+ () => JSON.stringify([boardView, [...hiddenIdentityIds].sort()]),
42049
+ [boardView, hiddenIdentityIds]
42050
+ );
42051
+ const shownRef = (0, import_react15.useRef)({ timeline, commits });
42052
+ const heldKey = (0, import_react15.useRef)(null);
42053
+ const heldHistoryId = (0, import_react15.useRef)(null);
42054
+ const awaiting = (0, import_react15.useRef)(true);
42055
+ if (heldKey.current !== requestKey) {
42056
+ heldKey.current = requestKey;
42057
+ awaiting.current = true;
42058
+ }
42059
+ if (awaiting.current && heldHistoryId.current !== historyId) {
42060
+ heldHistoryId.current = historyId;
42061
+ shownRef.current = { timeline, commits };
42062
+ awaiting.current = false;
42063
+ }
42064
+ const shown = shownRef.current;
42065
+ const axis = (0, import_react15.useMemo)(() => buildAxis(shown.timeline, shown.commits), [shown]);
42066
+ const categoriesFiltered = (timeline?.events.length ?? 0) > 0;
42067
+ const identities = (0, import_react15.useMemo)(
42068
+ () => listIdentities(shown.timeline, boardView),
42069
+ [shown, boardView]
42070
+ );
42071
+ const soleIdentity = (0, import_react15.useMemo)(
42072
+ () => soleVisibleIdentity(identities, hiddenIdentityIds),
42073
+ [identities, hiddenIdentityIds]
42074
+ );
42075
+ const issueCounts = bucketIssueCounts(
42076
+ axis,
42077
+ shown.timeline,
42078
+ boardView,
42079
+ hiddenIdentityIds
42080
+ );
42081
+ const commitStats = bucketCommitStats(axis, shown.commits);
42082
+ const liveEventDots = (0, import_react15.useMemo)(
42083
+ () => buildEventDots(shown.timeline, boardView, hiddenIdentityIds),
42084
+ [shown, boardView, hiddenIdentityIds]
42085
+ );
42086
+ const dragging = dragFraction !== null;
42087
+ const commitPoints = (0, import_react15.useMemo)(
42088
+ () => shown.commits.map((commit) => ({
42089
+ key: commit.sha,
42090
+ t: commit.time,
42091
+ fraction: axis.fractionForTime(commit.time),
42092
+ hourFraction: hourFractionForTime(commit.time),
42093
+ radius: 2,
42094
+ color: GUI_THEME.green,
42095
+ opacity: 0.55,
42096
+ // The hint labels the moment itself, so this is the rest of it.
42097
+ title: `${commit.subject} \u2014 ${commit.author} (${commit.linesChanged.toLocaleString()} lines)`,
42098
+ commitSha: commit.sha
42099
+ })),
42100
+ [shown.commits, axis]
42101
+ );
42102
+ const issuePoints = (0, import_react15.useMemo)(
42103
+ () => liveEventDots.map((dot) => ({
42104
+ key: dot.key,
42105
+ t: dot.t,
42106
+ fraction: axis.fractionForTime(dot.t),
42107
+ hourFraction: hourFractionForTime(dot.t),
42108
+ radius: dot.size / 2,
42109
+ color: dot.color,
42110
+ opacity: dot.opacity,
42111
+ title: dotDetail(dot),
42112
+ commitSha: null
42113
+ })),
42114
+ [liveEventDots, axis]
42115
+ );
42116
+ const boardFilter = (0, import_react15.useMemo)(
42117
+ () => buildBoardFilter(boardView, identities, hiddenIdentityIds),
42118
+ [boardView, identities, hiddenIdentityIds]
41440
42119
  );
42120
+ (0, import_react15.useEffect)(() => {
42121
+ onBoardFilterChange(boardFilter);
42122
+ }, [boardFilter]);
42123
+ const maxIssueBucketCount = maxOf(issueCounts, 1);
41441
42124
  const maxCommitCount = maxOf(
41442
42125
  Array.from(commitStats.values(), (stats) => stats.count),
41443
42126
  1
@@ -41455,7 +42138,12 @@ var TimeScrubber = ({
41455
42138
  const armEntrance = () => {
41456
42139
  armed.current = true;
41457
42140
  };
41458
- (0, import_react15.useEffect)(armEntrance, [boardId]);
42141
+ const armedBoardId = (0, import_react15.useRef)(boardId);
42142
+ (0, import_react15.useEffect)(() => {
42143
+ if (boardId === null || boardId === armedBoardId.current) return;
42144
+ armedBoardId.current = boardId;
42145
+ armEntrance();
42146
+ }, [boardId]);
41459
42147
  (0, import_react15.useEffect)(() => {
41460
42148
  if (seenHistoryId.current === historyId) return;
41461
42149
  seenHistoryId.current = historyId;
@@ -41463,7 +42151,28 @@ var TimeScrubber = ({
41463
42151
  armed.current = false;
41464
42152
  setEntrance((generation) => generation + 1);
41465
42153
  }, [historyId]);
41466
- const windowKey = `${layoutMode}-${entrance}`;
42154
+ const windowKey = `${layoutMode}-${entrance}-${filterKey}`;
42155
+ const scatterLayers = (0, import_react15.useMemo)(
42156
+ () => [
42157
+ ...commitScatter.mounted ? [
42158
+ {
42159
+ id: "commits",
42160
+ points: commitPoints,
42161
+ generation: windowKey,
42162
+ leaving: commitScatter.leaving
42163
+ }
42164
+ ] : [],
42165
+ ...issueScatter.mounted ? [
42166
+ {
42167
+ id: "issues",
42168
+ points: issuePoints,
42169
+ generation: windowKey,
42170
+ leaving: issueScatter.leaving
42171
+ }
42172
+ ] : []
42173
+ ],
42174
+ [commitPoints, issuePoints, commitScatter, issueScatter, windowKey]
42175
+ );
41467
42176
  const confirmedFraction = timeTravel.mode === "scrub" && timeTravel.asOfTime !== null ? axis.fractionForTime(timeTravel.asOfTime) : 1;
41468
42177
  const fractionFromClientX = (clientX) => {
41469
42178
  const track = trackRef.current;
@@ -41493,7 +42202,30 @@ var TimeScrubber = ({
41493
42202
  );
41494
42203
  const bucketTimeAt = (index2) => index2 !== null && index2 < axis.bucketCount ? axis.bucketTimeAt(index2) : void 0;
41495
42204
  const centreFraction = (index2) => (index2 + 0.5) / axis.bucketCount;
41496
- const boardEventRow = (count) => `${count} board event${count === 1 ? "" : "s"}`;
42205
+ const onScatterPointEnter = (0, import_react15.useCallback)(
42206
+ (point4) => {
42207
+ if (point4.commitSha) {
42208
+ const commit = shown.commits.find((c) => c.sha === point4.commitSha);
42209
+ if (commit) {
42210
+ setHoveredCommit({ commit, fraction: point4.fraction });
42211
+ setHoveredEvent(null);
42212
+ }
42213
+ return;
42214
+ }
42215
+ setHoveredCommit(null);
42216
+ setHoveredEvent({
42217
+ label: formatDateTime(new Date(point4.t)),
42218
+ detail: point4.title,
42219
+ t: point4.t,
42220
+ fraction: point4.fraction
42221
+ });
42222
+ },
42223
+ [shown.commits]
42224
+ );
42225
+ const onScatterPointLeave = (0, import_react15.useCallback)(() => {
42226
+ setHoveredEvent(null);
42227
+ setHoveredCommit(null);
42228
+ }, []);
41497
42229
  const hoveredBucketTime = bucketTimeAt(hoveredBucketIndex);
41498
42230
  const hoveredBucketCount = hoveredBucketIndex !== null ? issueCounts[hoveredBucketIndex] : void 0;
41499
42231
  const boardHint = layoutMode === "even" ? hoveredBucketTime !== void 0 && hoveredBucketCount !== void 0 ? {
@@ -41506,7 +42238,7 @@ var TimeScrubber = ({
41506
42238
  empty: hoveredBucketCount === 0
41507
42239
  } : null : hoveredEvent ? {
41508
42240
  label: hoveredEvent.label,
41509
- rows: [boardEventRow(hoveredEvent.count)],
42241
+ rows: [hoveredEvent.detail],
41510
42242
  fraction: hoveredEvent.fraction
41511
42243
  } : null;
41512
42244
  const hoveredCommitStats = hoveredCommitBucketIndex !== null ? commitStats.get(hoveredCommitBucketIndex) : void 0;
@@ -41557,6 +42289,17 @@ var TimeScrubber = ({
41557
42289
  onChangeShowIssues: setShowIssues,
41558
42290
  onChangeShowCommits: setShowCommits,
41559
42291
  onChangeAllBoards: changeAllBoards,
42292
+ boardView,
42293
+ identities,
42294
+ hiddenIdentityIds,
42295
+ categoriesExpanded,
42296
+ identitiesExpanded,
42297
+ categoriesFiltered,
42298
+ onChangeBoardView: changeBoardView,
42299
+ onToggleIdentity: toggleIdentity,
42300
+ onOnlyIdentity: isolateIdentity,
42301
+ onToggleCategoriesExpanded: () => setCategoriesExpanded(!categoriesExpanded),
42302
+ onSetIdentitiesExpanded: setIdentitiesExpanded,
41560
42303
  onReturnToLive
41561
42304
  },
41562
42305
  chart: {
@@ -41573,9 +42316,12 @@ var TimeScrubber = ({
41573
42316
  issueBarRange: populatedRange(issueBars),
41574
42317
  commitBars,
41575
42318
  commitBarRange: populatedRange(commitBars),
41576
- eventBuckets,
41577
- maxEventCount,
41578
- commits,
42319
+ scatterLayers,
42320
+ issueSeriesColor: soleIdentity?.color ?? boardViewColor(boardView),
42321
+ dragging,
42322
+ // Exits still need their animation, so this only silences a series
42323
+ // that has finished arriving.
42324
+ commits: shown.commits,
41579
42325
  hoveredCommitSha: hoveredCommit?.commit.sha ?? null,
41580
42326
  hoveredBucketIndex,
41581
42327
  hoveredCommitBucketIndex,
@@ -41615,18 +42361,8 @@ var TimeScrubber = ({
41615
42361
  setHoveredCommitBucketIndex(bucketIndexFromEvent(event));
41616
42362
  },
41617
42363
  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),
42364
+ onScatterPointEnter,
42365
+ onScatterPointLeave,
41630
42366
  onInspectCommit
41631
42367
  }
41632
42368
  }
@@ -41717,8 +42453,8 @@ var nodeRef = (id) => id.slice(-NODE_REF_LENGTH).toUpperCase();
41717
42453
  // source/gui/client/lib/gui-state-helper.tsx
41718
42454
  var getResultValue = (payload) => {
41719
42455
  if (!payload) return void 0;
41720
- if (typeof payload === "object" && payload !== null && "value" in payload && payload.value) {
41721
- return payload.value;
42456
+ if (typeof payload === "object" && payload !== null && "value" in payload) {
42457
+ return payload.value ?? void 0;
41722
42458
  }
41723
42459
  if (typeof payload === "object" && payload !== null && "content" in payload && payload.content?.[0]?.text) {
41724
42460
  return JSON.parse(payload.content[0].text).value;
@@ -41911,6 +42647,7 @@ var blobToBase64 = (blob) => new Promise((resolve, reject) => {
41911
42647
 
41912
42648
  // source/gui/client/App.tsx
41913
42649
  var import_jsx_runtime33 = __toESM(require_jsx_runtime(), 1);
42650
+ var EMPTY_COMMENTS = {};
41914
42651
  var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
41915
42652
  "div",
41916
42653
  {
@@ -41955,7 +42692,7 @@ var App = () => {
41955
42692
  const [mutationGate] = (0, import_react16.useState)(createMutationGate);
41956
42693
  const selectedTab = searchParams.get("tab") === "comments" ? "comments" : "overview";
41957
42694
  const navigate = useNavigate();
41958
- const selectedBoard = (state && boardId ? findBoard(state, boardId) : null) ?? state?.boards[0] ?? null;
42695
+ const selectedBoard = (state && boardId ? findBoard(state, boardId) : null) ?? state?.boards?.[0] ?? null;
41959
42696
  const selectedBoardId = selectedBoard?.id ?? null;
41960
42697
  const selectedBoardIdRef = (0, import_react16.useRef)(selectedBoardId);
41961
42698
  selectedBoardIdRef.current = selectedBoardId;
@@ -41980,9 +42717,38 @@ var App = () => {
41980
42717
  if (!boardSlug) return;
41981
42718
  void navigate(`/board/${boardSlug}`);
41982
42719
  };
41983
- const commentsByIssueId = state?.commentsByIssueId ?? {};
42720
+ const commentsByIssueId = state?.commentsByIssueId ?? EMPTY_COMMENTS;
42721
+ const [issueDetail, setIssueDetail] = (0, import_react16.useState)(null);
42722
+ const [boardFilter, setBoardFilter] = (0, import_react16.useState)(null);
42723
+ const { visibleSwimlanes, hiddenIssueCount } = (0, import_react16.useMemo)(() => {
42724
+ const swimlanes = selectedBoard?.swimlanes ?? [];
42725
+ if (!boardFilter) return { visibleSwimlanes: swimlanes, hiddenIssueCount: 0 };
42726
+ let hidden = 0;
42727
+ const visible = swimlanes.map((swimlane) => {
42728
+ const issues = swimlane.issues.filter(
42729
+ (issue) => issuePassesBoardFilter(
42730
+ issue,
42731
+ (commentsByIssueId[issue.id] ?? []).map((comment) => comment.author.id),
42732
+ boardFilter
42733
+ )
42734
+ );
42735
+ hidden += swimlane.issues.length - issues.length;
42736
+ return { ...swimlane, issues };
42737
+ });
42738
+ return { visibleSwimlanes: visible, hiddenIssueCount: hidden };
42739
+ }, [selectedBoard, boardFilter, commentsByIssueId]);
41984
42740
  const attachmentsByIssueId = state?.attachmentsByIssueId ?? {};
41985
42741
  const [attachmentUploadStatus, setAttachmentUploadStatus] = (0, import_react16.useState)({ state: "idle" });
42742
+ (0, import_react16.useEffect)(() => {
42743
+ if (!selectedIssue) {
42744
+ setIssueDetail(null);
42745
+ return;
42746
+ }
42747
+ sendSocketJson(socketRef.current, {
42748
+ type: "issue:get",
42749
+ payload: { issueId: selectedIssue.id }
42750
+ });
42751
+ }, [selectedIssue?.id, state]);
41986
42752
  const requestState = () => {
41987
42753
  sendSocketJson(socketRef.current, { type: "state:get" });
41988
42754
  };
@@ -42010,6 +42776,10 @@ var App = () => {
42010
42776
  const nextState = getResultValue(message.payload);
42011
42777
  if (nextState) setState(nextState);
42012
42778
  }
42779
+ if (message.type === "issue") {
42780
+ const detail = getResultValue(message.payload);
42781
+ if (detail) setIssueDetail(detail);
42782
+ }
42013
42783
  if (message.type === "issue:created") {
42014
42784
  const created = getResultValue(message.payload);
42015
42785
  if (created && boardId) {
@@ -42070,8 +42840,9 @@ var App = () => {
42070
42840
  };
42071
42841
  }, [boardId, navigate]);
42072
42842
  (0, import_react16.useEffect)(() => {
42073
- if (!boardId && state?.boards[0]) {
42074
- void navigate(`/board/${state.boards[0].ref}`, { replace: true });
42843
+ const first = state?.boards?.[0];
42844
+ if (!boardId && first) {
42845
+ void navigate(`/board/${first.ref}`, { replace: true });
42075
42846
  }
42076
42847
  }, [boardId, state, navigate]);
42077
42848
  (0, import_react16.useEffect)(() => {
@@ -42468,7 +43239,8 @@ var App = () => {
42468
43239
  onInspectCommit: inspectCommit,
42469
43240
  timeTravel: state?.timeTravel ?? { mode: "live", asOfTime: null },
42470
43241
  onScrub: scrubToTime,
42471
- onReturnToLive: returnToLive
43242
+ onReturnToLive: returnToLive,
43243
+ onBoardFilterChange: setBoardFilter
42472
43244
  }
42473
43245
  ),
42474
43246
  /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
@@ -42495,23 +43267,34 @@ var App = () => {
42495
43267
  overflow: "hidden"
42496
43268
  },
42497
43269
  children: [
42498
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { style: { padding: "20px 10px" }, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42499
- Dropdown,
43270
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43271
+ "div",
42500
43272
  {
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
43273
+ style: {
43274
+ padding: "20px 10px",
43275
+ display: "flex",
43276
+ alignItems: "center",
43277
+ gap: 10
43278
+ },
43279
+ children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43280
+ Dropdown,
43281
+ {
43282
+ testId: "board-switcher",
43283
+ label: "Board:",
43284
+ value: selectedBoard ? {
43285
+ id: selectedBoard.id,
43286
+ label: selectedBoard.title
43287
+ } : null,
43288
+ items: state?.boards.map((board) => ({
43289
+ id: board.id,
43290
+ label: board.title
43291
+ })) ?? [],
43292
+ placeholder: "Loading...",
43293
+ onSelect: selectBoard
43294
+ }
43295
+ )
42513
43296
  }
42514
- ) }),
43297
+ ),
42515
43298
  /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
42516
43299
  "div",
42517
43300
  {
@@ -42524,7 +43307,7 @@ var App = () => {
42524
43307
  overflowY: "hidden"
42525
43308
  },
42526
43309
  children: [
42527
- selectedBoard?.swimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43310
+ visibleSwimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42528
43311
  SwimlaneColumn,
42529
43312
  {
42530
43313
  swimlane,
@@ -42591,9 +43374,9 @@ var App = () => {
42591
43374
  IssueDetails,
42592
43375
  {
42593
43376
  whoAmI: state.user,
42594
- issue: selectedIssue,
43377
+ issue: issueDetail?.issueId === selectedIssue.id ? { ...selectedIssue, description: issueDetail.description } : selectedIssue,
42595
43378
  activeTab: selectedTab,
42596
- comments: commentsByIssueId[selectedIssue.id] ?? [],
43379
+ comments: issueDetail?.issueId === selectedIssue.id ? issueDetail.comments : [],
42597
43380
  onChangeTab: changeIssueDetailsTab,
42598
43381
  onClose: closeIssueDetails,
42599
43382
  onEditTitle: editIssueTitle,