epiq 1.4.1 → 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
@@ -22849,6 +22849,19 @@ function useLocation() {
22849
22849
  );
22850
22850
  return React2.useContext(LocationContext).location;
22851
22851
  }
22852
+ function useMatch(pattern) {
22853
+ invariant(
22854
+ useInRouterContext(),
22855
+ // TODO: This error is probably because they somehow have 2 versions of the
22856
+ // router loaded. We can help them understand how to avoid that.
22857
+ `useMatch() may be used only in the context of a <Router> component.`
22858
+ );
22859
+ let { pathname } = useLocation();
22860
+ return React2.useMemo(
22861
+ () => matchPath(pattern, decodePath(pathname)),
22862
+ [pathname, pattern]
22863
+ );
22864
+ }
22852
22865
  var navigateEffectWarning = `You should call navigate() in a React.useEffect(), not when your component is first rendered.`;
22853
22866
  function useIsomorphicLayoutEffect(cb) {
22854
22867
  let isStatic = React2.useContext(NavigationContext).static;
@@ -24825,7 +24838,7 @@ function useViewTransitionState(to, { relative } = {}) {
24825
24838
  }
24826
24839
 
24827
24840
  // source/gui/client/App.tsx
24828
- var import_react15 = __toESM(require_react(), 1);
24841
+ var import_react16 = __toESM(require_react(), 1);
24829
24842
 
24830
24843
  // source/gui/client/components/Aside.tsx
24831
24844
  var import_react = __toESM(require_react(), 1);
@@ -24847,6 +24860,12 @@ var GUI_THEME = {
24847
24860
  green: "#8ce99a",
24848
24861
  red: "#ff8787"
24849
24862
  };
24863
+ var EVENT_CATEGORY_COLORS = {
24864
+ tickets: GUI_THEME.accent,
24865
+ comments: "#ffd479",
24866
+ tagging: "#c9a5ff",
24867
+ assigning: "#ff9ecd"
24868
+ };
24850
24869
 
24851
24870
  // source/gui/client/components/Aside.tsx
24852
24871
  var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
@@ -25305,7 +25324,7 @@ var User = ({
25305
25324
  );
25306
25325
 
25307
25326
  // source/version.ts
25308
- var EPIQ_VERSION = "1.4.1";
25327
+ var EPIQ_VERSION = "1.4.3";
25309
25328
 
25310
25329
  // source/gui/client/components/Header.tsx
25311
25330
  var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
@@ -27702,11 +27721,11 @@ function addChildren(props, children) {
27702
27721
  }
27703
27722
  }
27704
27723
  }
27705
- function productionCreate(_, jsx34, jsxs22) {
27724
+ function productionCreate(_, jsx35, jsxs23) {
27706
27725
  return create2;
27707
27726
  function create2(_2, type, props, key) {
27708
27727
  const isStaticChildren = Array.isArray(props.children);
27709
- const fn = isStaticChildren ? jsxs22 : jsx34;
27728
+ const fn = isStaticChildren ? jsxs23 : jsx35;
27710
27729
  return key ? fn(type, props, key) : fn(type, props);
27711
27730
  }
27712
27731
  }
@@ -38842,6 +38861,9 @@ var MarkdownContent = ({
38842
38861
  }
38843
38862
  );
38844
38863
 
38864
+ // source/lib/utils/comment.limits.ts
38865
+ var MAX_COMMENT_LENGTH = 4e3;
38866
+
38845
38867
  // source/gui/client/components/IssueComments.tsx
38846
38868
  var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
38847
38869
  var IssueComments = ({
@@ -38853,9 +38875,11 @@ var IssueComments = ({
38853
38875
  onDeleteComment
38854
38876
  }) => {
38855
38877
  const [body, setBody] = (0, import_react9.useState)("");
38878
+ const length = body.trim().length;
38879
+ const tooLong = length > MAX_COMMENT_LENGTH;
38856
38880
  const addComment = () => {
38857
38881
  const nextBody = body.trim();
38858
- if (!nextBody) return;
38882
+ if (!nextBody || tooLong) return;
38859
38883
  onAddComment?.(issueId, nextBody);
38860
38884
  setBody("");
38861
38885
  };
@@ -38912,7 +38936,7 @@ var IssueComments = ({
38912
38936
  /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
38913
38937
  Textarea,
38914
38938
  {
38915
- maxLength: 120,
38939
+ maxLength: Number.MAX_SAFE_INTEGER,
38916
38940
  value: body,
38917
38941
  placeholder: "write a comment",
38918
38942
  onChange: (event) => setBody(event.target.value),
@@ -38929,7 +38953,20 @@ var IssueComments = ({
38929
38953
  }
38930
38954
  }
38931
38955
  ),
38932
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ActionRow, { children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Button, { onClick: addComment, children: "comment" }) })
38956
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(ActionRow, { children: [
38957
+ length > MAX_COMMENT_LENGTH / 2 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
38958
+ "span",
38959
+ {
38960
+ style: {
38961
+ alignSelf: "center",
38962
+ fontSize: 11,
38963
+ color: tooLong ? GUI_THEME.red : GUI_THEME.dim
38964
+ },
38965
+ children: tooLong ? `${length - MAX_COMMENT_LENGTH} over the limit` : `${MAX_COMMENT_LENGTH - length} left`
38966
+ }
38967
+ ),
38968
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Button, { disabled: tooLong, onClick: addComment, children: "comment" })
38969
+ ] })
38933
38970
  ] })
38934
38971
  ] });
38935
38972
  };
@@ -39446,12 +39483,177 @@ var IssueDetails = ({
39446
39483
  ] });
39447
39484
  };
39448
39485
 
39486
+ // source/gui/client/components/BulkDetails.tsx
39487
+ var import_jsx_runtime21 = __toESM(require_jsx_runtime(), 1);
39488
+ var countBy = (issues, pick) => {
39489
+ const byId = /* @__PURE__ */ new Map();
39490
+ for (const issue of issues) {
39491
+ for (const item of pick(issue)) {
39492
+ const seen = byId.get(item.id);
39493
+ byId.set(item.id, { item, count: (seen?.count ?? 0) + 1 });
39494
+ }
39495
+ }
39496
+ return [...byId.values()];
39497
+ };
39498
+ var BulkDetails = ({
39499
+ issues,
39500
+ knownTags,
39501
+ knownAssignees,
39502
+ tagName,
39503
+ assigneeName,
39504
+ onChangeTagName,
39505
+ onChangeAssigneeName,
39506
+ onAddTag,
39507
+ onRemoveTag,
39508
+ onAddAssignee,
39509
+ onRemoveAssignee,
39510
+ onCloseIssues,
39511
+ onReopenIssues,
39512
+ onClear
39513
+ }) => {
39514
+ const total = issues.length;
39515
+ const tags = countBy(issues, (issue) => issue.tags);
39516
+ const assignees = countBy(issues, (issue) => issue.assignees);
39517
+ const availableTags = knownTags.filter(
39518
+ (tag) => !tags.some((entry) => entry.item.id === tag.id && entry.count === total)
39519
+ );
39520
+ const availableAssignees = knownAssignees.filter(
39521
+ (person) => !assignees.some(
39522
+ (entry) => entry.item.id === person.id && entry.count === total
39523
+ )
39524
+ );
39525
+ const partial = (count) => count === total ? "" : ` ${count}/${total}`;
39526
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Aside, { children: [
39527
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
39528
+ "div",
39529
+ {
39530
+ style: {
39531
+ display: "flex",
39532
+ alignItems: "center",
39533
+ justifyContent: "space-between",
39534
+ gap: 8,
39535
+ marginBottom: 12
39536
+ },
39537
+ children: [
39538
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { style: { fontSize: 13, fontWeight: 600, color: GUI_THEME.primary }, children: [
39539
+ total,
39540
+ " tickets selected"
39541
+ ] }),
39542
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Button, { variant: "ghost", onClick: onClear, children: "clear" })
39543
+ ]
39544
+ }
39545
+ ),
39546
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Section, { title: "Tags", children: [
39547
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ChipRow, { children: tags.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Empty, { children: "No tags" }) : tags.map(({ item, count }) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
39548
+ Button,
39549
+ {
39550
+ variant: "chip",
39551
+ title: "Remove from every selected ticket",
39552
+ onClick: () => onRemoveTag(item.id),
39553
+ style: { color: item.color },
39554
+ children: [
39555
+ item.name,
39556
+ partial(count),
39557
+ " \xD7"
39558
+ ]
39559
+ },
39560
+ item.id
39561
+ )) }),
39562
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
39563
+ Button,
39564
+ {
39565
+ variant: "chip",
39566
+ title: "Add to every selected ticket",
39567
+ onClick: () => onAddTag(tag.name),
39568
+ style: { color: tag.color, opacity: 0.6 },
39569
+ children: [
39570
+ "+ ",
39571
+ tag.name
39572
+ ]
39573
+ },
39574
+ tag.id
39575
+ )) }),
39576
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(AddRow, { children: [
39577
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
39578
+ Input,
39579
+ {
39580
+ value: tagName,
39581
+ placeholder: "new tag",
39582
+ onChange: (event) => onChangeTagName(event.target.value),
39583
+ onKeyDown: (event) => {
39584
+ if (event.key === "Enter" && tagName.trim()) {
39585
+ onAddTag(tagName.trim());
39586
+ }
39587
+ }
39588
+ }
39589
+ ),
39590
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Button, { onClick: () => tagName.trim() && onAddTag(tagName.trim()), children: "add" })
39591
+ ] })
39592
+ ] }),
39593
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Section, { title: "Assignees", children: [
39594
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ChipRow, { children: assignees.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Empty, { children: "No assignees" }) : assignees.map(({ item, count }) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
39595
+ Button,
39596
+ {
39597
+ variant: "chip",
39598
+ title: "Remove from every selected ticket",
39599
+ onClick: () => onRemoveAssignee(item.id),
39600
+ style: { color: item.color },
39601
+ children: [
39602
+ "@",
39603
+ item.name,
39604
+ partial(count),
39605
+ " \xD7"
39606
+ ]
39607
+ },
39608
+ item.id
39609
+ )) }),
39610
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ChipRow, { children: availableAssignees.map((person) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
39611
+ Button,
39612
+ {
39613
+ variant: "chip",
39614
+ title: "Add to every selected ticket",
39615
+ onClick: () => onAddAssignee(person.id),
39616
+ style: { color: person.color, opacity: 0.6 },
39617
+ children: [
39618
+ "+ @",
39619
+ person.isSelf ? "me" : person.name
39620
+ ]
39621
+ },
39622
+ person.id
39623
+ )) }),
39624
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(AddRow, { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
39625
+ Input,
39626
+ {
39627
+ value: assigneeName,
39628
+ placeholder: "name of unknown contributor",
39629
+ onChange: (event) => onChangeAssigneeName(event.target.value)
39630
+ }
39631
+ ) })
39632
+ ] }),
39633
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Section, { title: "Actions", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(ChipRow, { children: [
39634
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { onClick: onCloseIssues, children: [
39635
+ "close ",
39636
+ total,
39637
+ " tickets"
39638
+ ] }),
39639
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { onClick: onReopenIssues, children: [
39640
+ "reopen ",
39641
+ total,
39642
+ " tickets"
39643
+ ] })
39644
+ ] }) })
39645
+ ] });
39646
+ };
39647
+
39449
39648
  // source/gui/client/components/SwimlaneColumn.tsx
39649
+ var import_react12 = __toESM(require_react(), 1);
39650
+
39651
+ // source/gui/client/components/TicketCard.tsx
39450
39652
  var import_react11 = __toESM(require_react(), 1);
39451
39653
 
39452
39654
  // source/gui/client/components/IconComment.tsx
39453
- var import_jsx_runtime21 = __toESM(require_jsx_runtime(), 1);
39454
- var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
39655
+ var import_jsx_runtime22 = __toESM(require_jsx_runtime(), 1);
39656
+ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39455
39657
  "svg",
39456
39658
  {
39457
39659
  width: size,
@@ -39459,7 +39661,7 @@ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime21.js
39459
39661
  viewBox: "0 0 16 16",
39460
39662
  fill: "none",
39461
39663
  "aria-hidden": "true",
39462
- children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
39664
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39463
39665
  "path",
39464
39666
  {
39465
39667
  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",
@@ -39474,28 +39676,40 @@ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime21.js
39474
39676
  );
39475
39677
 
39476
39678
  // source/gui/client/components/TicketCard.tsx
39477
- var import_jsx_runtime22 = __toESM(require_jsx_runtime(), 1);
39679
+ var import_jsx_runtime23 = __toESM(require_jsx_runtime(), 1);
39478
39680
  var TicketCard = ({
39479
39681
  ticket,
39480
39682
  index: index2,
39481
39683
  isSelected,
39684
+ isPicked,
39482
39685
  commentCount,
39483
39686
  onOpenComments,
39484
39687
  onSelect,
39485
- onDragStart,
39486
39688
  onDragOverIssue,
39487
39689
  onDropIssueAt
39488
39690
  }) => {
39691
+ const cardRef = (0, import_react11.useRef)(null);
39692
+ (0, import_react11.useEffect)(() => {
39693
+ if (!isSelected) return;
39694
+ cardRef.current?.scrollIntoView({
39695
+ behavior: "smooth",
39696
+ block: "nearest",
39697
+ inline: "nearest"
39698
+ });
39699
+ }, [isSelected]);
39489
39700
  const getVisualTargetIndex = (isAfterMiddle) => index2 + (isAfterMiddle ? 1 : 0);
39490
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
39701
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
39491
39702
  "div",
39492
39703
  {
39704
+ ref: cardRef,
39493
39705
  draggable: !ticket.readonly,
39494
- onClick: onSelect,
39706
+ onClick: (event) => {
39707
+ event.stopPropagation();
39708
+ onSelect({ toggle: event.metaKey || event.ctrlKey || event.shiftKey });
39709
+ },
39495
39710
  onDragStart: (event) => {
39496
39711
  event.dataTransfer.effectAllowed = "move";
39497
39712
  event.dataTransfer.setData("text/plain", ticket.id);
39498
- onDragStart(ticket.id);
39499
39713
  },
39500
39714
  onDragOver: (event) => {
39501
39715
  event.preventDefault();
@@ -39517,18 +39731,22 @@ var TicketCard = ({
39517
39731
  display: "flex",
39518
39732
  alignItems: "flex-start",
39519
39733
  gap: 10,
39520
- color: isSelected ? GUI_THEME.accent : GUI_THEME.primary,
39734
+ color: isSelected || isPicked ? GUI_THEME.accent : GUI_THEME.primary,
39521
39735
  fontSize: 11,
39522
39736
  cursor: ticket.readonly ? "default" : "grab",
39523
- background: isSelected ? "rgba(118,228,255,0.08)" : "rgba(185, 192, 255, 0.06)",
39737
+ background: isSelected || isPicked ? "rgba(118,228,255,0.08)" : "rgba(185, 192, 255, 0.06)",
39524
39738
  padding: "10px 8px",
39525
39739
  minHeight: "58px",
39526
39740
  borderRadius: "8px",
39527
39741
  marginBottom: 4,
39528
- border: `1px solid ${isSelected ? GUI_THEME.accent : "transparent"}`
39742
+ border: `1px solid ${isSelected || isPicked ? GUI_THEME.accent : "transparent"}`,
39743
+ // Only the multi-selection is outlined, so it stays legible when the
39744
+ // details panel is closed.
39745
+ outline: isPicked ? `1px solid ${GUI_THEME.accent}` : void 0,
39746
+ outlineOffset: 1
39529
39747
  },
39530
39748
  children: [
39531
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39749
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39532
39750
  "div",
39533
39751
  {
39534
39752
  style: {
@@ -39542,7 +39760,7 @@ var TicketCard = ({
39542
39760
  children: isSelected ? "\u276F" : index2 + 1
39543
39761
  }
39544
39762
  ),
39545
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
39763
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
39546
39764
  "div",
39547
39765
  {
39548
39766
  style: {
@@ -39554,7 +39772,7 @@ var TicketCard = ({
39554
39772
  paddingLeft: 8
39555
39773
  },
39556
39774
  children: [
39557
- /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
39775
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
39558
39776
  "div",
39559
39777
  {
39560
39778
  style: {
@@ -39565,7 +39783,7 @@ var TicketCard = ({
39565
39783
  flex: 1
39566
39784
  },
39567
39785
  children: [
39568
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39786
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39569
39787
  "div",
39570
39788
  {
39571
39789
  style: {
@@ -39581,17 +39799,17 @@ var TicketCard = ({
39581
39799
  children: ticket.title
39582
39800
  }
39583
39801
  ),
39584
- ticket.ref && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39802
+ ticket.ref && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39585
39803
  "div",
39586
39804
  {
39587
39805
  style: {
39588
39806
  color: GUI_THEME.dim2,
39589
39807
  fontSize: 10
39590
39808
  },
39591
- children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(CopyRef, { refValue: ticket.ref })
39809
+ children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(CopyRef, { refValue: ticket.ref })
39592
39810
  }
39593
39811
  ),
39594
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39812
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39595
39813
  "div",
39596
39814
  {
39597
39815
  style: {
@@ -39600,7 +39818,7 @@ var TicketCard = ({
39600
39818
  gap: 6,
39601
39819
  alignItems: "center"
39602
39820
  },
39603
- children: ticket.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39821
+ children: ticket.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39604
39822
  "span",
39605
39823
  {
39606
39824
  style: {
@@ -39620,7 +39838,7 @@ var TicketCard = ({
39620
39838
  ]
39621
39839
  }
39622
39840
  ),
39623
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39841
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39624
39842
  "div",
39625
39843
  {
39626
39844
  style: {
@@ -39630,7 +39848,7 @@ var TicketCard = ({
39630
39848
  flexShrink: 0,
39631
39849
  paddingTop: 2
39632
39850
  },
39633
- children: commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
39851
+ children: commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
39634
39852
  "button",
39635
39853
  {
39636
39854
  type: "button",
@@ -39668,14 +39886,14 @@ var TicketCard = ({
39668
39886
  event.currentTarget.style.transform = "translateY(0)";
39669
39887
  },
39670
39888
  children: [
39671
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(IconComment, {}),
39672
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { children: commentCount })
39889
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(IconComment, {}),
39890
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { children: commentCount })
39673
39891
  ]
39674
39892
  }
39675
39893
  )
39676
39894
  }
39677
39895
  ),
39678
- ticket.assignees.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39896
+ ticket.assignees.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39679
39897
  "div",
39680
39898
  {
39681
39899
  style: {
@@ -39685,7 +39903,15 @@ var TicketCard = ({
39685
39903
  paddingTop: 2,
39686
39904
  marginTop: "-4px"
39687
39905
  },
39688
- children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime22.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
+ ))
39689
39915
  }
39690
39916
  )
39691
39917
  ]
@@ -39697,7 +39923,7 @@ var TicketCard = ({
39697
39923
  };
39698
39924
 
39699
39925
  // source/gui/client/components/SwimlaneColumn.tsx
39700
- var import_jsx_runtime23 = __toESM(require_jsx_runtime(), 1);
39926
+ var import_jsx_runtime24 = __toESM(require_jsx_runtime(), 1);
39701
39927
  var COLUMN_GLOW_COLOR = "rgb(140, 176, 232)";
39702
39928
  var COLUMN_PADDING = 14;
39703
39929
  var SCROLLBAR_GUTTER_INSET = COLUMN_PADDING / 2;
@@ -39710,13 +39936,14 @@ var SwimlaneColumn = ({
39710
39936
  dragOver,
39711
39937
  dropIndex,
39712
39938
  onSelectIssue,
39939
+ pickedIssueIds,
39713
39940
  onCreateIssue,
39714
39941
  onDropIssue,
39715
39942
  onDragOver,
39716
39943
  onDragOverIssue,
39717
39944
  onDragLeave
39718
39945
  }) => {
39719
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
39946
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
39720
39947
  Panel,
39721
39948
  {
39722
39949
  as: "section",
@@ -39763,7 +39990,7 @@ var SwimlaneColumn = ({
39763
39990
  onDragLeave();
39764
39991
  },
39765
39992
  children: [
39766
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
39993
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
39767
39994
  "header",
39768
39995
  {
39769
39996
  style: {
@@ -39775,7 +40002,7 @@ var SwimlaneColumn = ({
39775
40002
  alignItems: "center"
39776
40003
  },
39777
40004
  children: [
39778
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
40005
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
39779
40006
  "div",
39780
40007
  {
39781
40008
  style: {
@@ -39784,37 +40011,37 @@ var SwimlaneColumn = ({
39784
40011
  gap: 8
39785
40012
  },
39786
40013
  children: [
39787
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40014
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39788
40015
  "span",
39789
40016
  {
39790
40017
  style: { color: selected ? GUI_THEME.accent : GUI_THEME.secondary },
39791
40018
  children: selected ? "\u276F" : " "
39792
40019
  }
39793
40020
  ),
39794
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40021
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39795
40022
  "strong",
39796
40023
  {
39797
40024
  style: { color: selected ? GUI_THEME.accent : GUI_THEME.primary },
39798
40025
  children: swimlane.title
39799
40026
  }
39800
40027
  ),
39801
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { style: { color: GUI_THEME.dim }, children: [
40028
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { style: { color: GUI_THEME.dim }, children: [
39802
40029
  "(",
39803
40030
  swimlane.issues.length,
39804
40031
  ")"
39805
40032
  ] }),
39806
- swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40033
+ swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39807
40034
  "span",
39808
40035
  {
39809
40036
  title: "Read-only",
39810
40037
  style: { display: "flex", color: GUI_THEME.dim },
39811
- children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(IconLock, {})
40038
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(IconLock, {})
39812
40039
  }
39813
40040
  )
39814
40041
  ]
39815
40042
  }
39816
40043
  ),
39817
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40044
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39818
40045
  Button,
39819
40046
  {
39820
40047
  variant: "ghost",
@@ -39827,7 +40054,7 @@ var SwimlaneColumn = ({
39827
40054
  ]
39828
40055
  }
39829
40056
  ),
39830
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40057
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39831
40058
  "div",
39832
40059
  {
39833
40060
  style: {
@@ -39838,9 +40065,9 @@ var SwimlaneColumn = ({
39838
40065
  flex: 1,
39839
40066
  minHeight: 0
39840
40067
  },
39841
- children: swimlane.issues.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
39842
- dropIndex === 0 && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(DropIndicator, {}),
39843
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40068
+ children: swimlane.issues.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
40069
+ dropIndex === 0 && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(DropIndicator, {}),
40070
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39844
40071
  "div",
39845
40072
  {
39846
40073
  style: {
@@ -39852,17 +40079,17 @@ var SwimlaneColumn = ({
39852
40079
  children: "Drop issue here"
39853
40080
  }
39854
40081
  )
39855
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_jsx_runtime23.Fragment, { children: [
39856
- swimlane.issues.map((ticket, index2) => /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(import_react11.default.Fragment, { children: [
39857
- dropIndex === index2 && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(DropIndicator, {}),
39858
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40082
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
40083
+ swimlane.issues.map((ticket, index2) => /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_react12.default.Fragment, { children: [
40084
+ dropIndex === index2 && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(DropIndicator, {}),
40085
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39859
40086
  TicketCard,
39860
40087
  {
39861
40088
  ticket,
39862
40089
  index: index2,
39863
40090
  isSelected: ticket.id === selectedIssueId,
39864
- onSelect: () => onSelectIssue(ticket.id),
39865
- onDragStart: (issueId) => onSelectIssue(issueId),
40091
+ isPicked: pickedIssueIds.includes(ticket.id),
40092
+ onSelect: (options) => onSelectIssue(ticket.id, options),
39866
40093
  onOpenComments: onSelectIssueComments,
39867
40094
  commentCount: commentsByIssueId[ticket.id]?.length ?? 0,
39868
40095
  onDragOverIssue: (targetIndex) => onDragOverIssue(swimlane.id, targetIndex),
@@ -39873,7 +40100,7 @@ var SwimlaneColumn = ({
39873
40100
  }
39874
40101
  )
39875
40102
  ] }, ticket.id)),
39876
- dropIndex === swimlane.issues.length && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(DropIndicator, {})
40103
+ dropIndex === swimlane.issues.length && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(DropIndicator, {})
39877
40104
  ] })
39878
40105
  }
39879
40106
  )
@@ -39883,9 +40110,9 @@ var SwimlaneColumn = ({
39883
40110
  };
39884
40111
 
39885
40112
  // source/gui/client/components/GlobalScrollbarStyles.tsx
39886
- var import_jsx_runtime24 = __toESM(require_jsx_runtime(), 1);
40113
+ var import_jsx_runtime25 = __toESM(require_jsx_runtime(), 1);
39887
40114
  var SCROLLBAR_SIZE = 3;
39888
- var GlobalScrollbarStyles = () => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("style", { children: `
40115
+ var GlobalScrollbarStyles = () => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("style", { children: `
39889
40116
  /* Gated deliberately: Chrome implements scrollbar-width/-color too, and
39890
40117
  setting either makes it ignore the ::-webkit-scrollbar rules below. */
39891
40118
  @supports not selector(::-webkit-scrollbar) {
@@ -39926,8 +40153,8 @@ var GlobalScrollbarStyles = () => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39926
40153
  ` });
39927
40154
 
39928
40155
  // source/gui/client/components/ErrorToast.tsx
39929
- var import_jsx_runtime25 = __toESM(require_jsx_runtime(), 1);
39930
- var ErrorToast = ({ message, onDismiss }) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
40156
+ var import_jsx_runtime26 = __toESM(require_jsx_runtime(), 1);
40157
+ var ErrorToast = ({ message, onDismiss }) => /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
39931
40158
  "div",
39932
40159
  {
39933
40160
  style: {
@@ -39948,8 +40175,8 @@ var ErrorToast = ({ message, onDismiss }) => /* @__PURE__ */ (0, import_jsx_runt
39948
40175
  boxShadow: "0 4px 16px rgba(0, 0, 0, 0.4)"
39949
40176
  },
39950
40177
  children: [
39951
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { style: { flex: 1, minWidth: 0, overflowWrap: "anywhere" }, children: message }),
39952
- /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
40178
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { style: { flex: 1, minWidth: 0, overflowWrap: "anywhere" }, children: message }),
40179
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
39953
40180
  "button",
39954
40181
  {
39955
40182
  onClick: onDismiss,
@@ -39970,7 +40197,7 @@ var ErrorToast = ({ message, onDismiss }) => /* @__PURE__ */ (0, import_jsx_runt
39970
40197
  );
39971
40198
 
39972
40199
  // source/gui/client/components/TimeScrubber.tsx
39973
- var import_react14 = __toESM(require_react(), 1);
40200
+ var import_react15 = __toESM(require_react(), 1);
39974
40201
 
39975
40202
  // source/lib/utils/date.utils.ts
39976
40203
  var pad = (n) => String(n).padStart(2, "0");
@@ -39983,14 +40210,14 @@ var minOf = (values, seed) => values.reduce((min, value) => value < min ? value
39983
40210
  var maxOf = (values, seed) => values.reduce((max, value) => value > max ? value : max, seed);
39984
40211
 
39985
40212
  // source/gui/client/lib/scrubber.ts
39986
- var import_react12 = __toESM(require_react(), 1);
40213
+ var import_react13 = __toESM(require_react(), 1);
39987
40214
  var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
39988
40215
  var TRACK_HEIGHT = 24;
39989
40216
  var EVENTS_MODE_TOTAL_HEIGHT = 8 + TRACK_HEIGHT * 2;
39990
40217
  var EVENTS_SCATTER_HEIGHT = TRACK_HEIGHT + 16;
39991
40218
  var EVENTS_MODE_VERTICAL_PADDING = (EVENTS_MODE_TOTAL_HEIGHT - EVENTS_SCATTER_HEIGHT) / 2;
39992
40219
  var HOVER_HINT_WIDTH = 220;
39993
- var SEGMENT_HIGHLIGHT_COLOR = "rgba(122, 158, 214, 0.07)";
40220
+ var SEGMENT_HIGHLIGHT_COLOR = "rgba(122, 157, 214, 0.14)";
39994
40221
  var BUCKET_HIGHLIGHT_COLOR = "rgba(255, 255, 255, 0.06)";
39995
40222
  var NEEDLE_COLOR = "rgba(255, 255, 255, 0.62)";
39996
40223
  var MIN_BUCKET_COUNT_FOR_GAP = 300;
@@ -40026,8 +40253,15 @@ var buildAxis = (timeline, commits, now = Date.now()) => {
40026
40253
  fractionToTime: (fraction) => Math.round(earliest + clamp(fraction, 0, 1) * span)
40027
40254
  };
40028
40255
  };
40029
- var bucketIssueCounts = (axis, timeline) => {
40256
+ var bucketIssueCounts = (axis, timeline, view = "all", hiddenIds = /* @__PURE__ */ new Set()) => {
40030
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
+ }
40031
40265
  for (const sparseBucket of timeline?.buckets ?? []) {
40032
40266
  counts[axis.bucketIndexForTime(sparseBucket.t)] += sparseBucket.count;
40033
40267
  }
@@ -40045,6 +40279,102 @@ var bucketCommitStats = (axis, commits) => {
40045
40279
  }
40046
40280
  return byIndex;
40047
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
+ };
40048
40378
  var populatedRange = (bars) => bars.length > 0 ? [
40049
40379
  bars.reduce((min, bar) => Math.min(min, bar.index), Infinity),
40050
40380
  bars.reduce((max, bar) => Math.max(max, bar.index), -Infinity)
@@ -40053,8 +40383,9 @@ var hourFractionForTime = (time) => {
40053
40383
  const date = new Date(time);
40054
40384
  return (date.getHours() * 60 + date.getMinutes()) / (24 * 60);
40055
40385
  };
40056
- var SEGMENT_UNIT_ORDER = ["day", "week", "month", "year"];
40386
+ var SEGMENT_UNIT_ORDER = ["hour", "day", "week", "month", "year"];
40057
40387
  var APPROX_UNIT_MS = {
40388
+ hour: 60 * 60 * 1e3,
40058
40389
  day: 24 * 60 * 60 * 1e3,
40059
40390
  week: 7 * 24 * 60 * 60 * 1e3,
40060
40391
  month: 30.44 * 24 * 60 * 60 * 1e3,
@@ -40080,14 +40411,16 @@ var MONTH_LABELS = [
40080
40411
  "Dec"
40081
40412
  ];
40082
40413
  var advanceByUnit = (date, unit) => {
40083
- 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);
40084
40416
  else if (unit === "week") date.setDate(date.getDate() + 7);
40085
40417
  else if (unit === "month") date.setMonth(date.getMonth() + 1);
40086
40418
  else date.setFullYear(date.getFullYear() + 1);
40087
40419
  };
40088
40420
  var segmentAt = (time, unit) => {
40089
40421
  const start2 = new Date(time);
40090
- start2.setHours(0, 0, 0, 0);
40422
+ if (unit === "hour") start2.setMinutes(0, 0, 0);
40423
+ else start2.setHours(0, 0, 0, 0);
40091
40424
  if (unit === "week") {
40092
40425
  start2.setDate(start2.getDate() - (start2.getDay() + 6) % 7);
40093
40426
  } else if (unit === "month") {
@@ -40098,7 +40431,10 @@ var segmentAt = (time, unit) => {
40098
40431
  const end = new Date(start2);
40099
40432
  advanceByUnit(end, unit);
40100
40433
  const lastDay = new Date(end.getTime() - 1);
40101
- 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()}`;
40102
40438
  return { start: start2.getTime(), end: end.getTime(), label };
40103
40439
  };
40104
40440
  var formatInterval = (start2, end) => {
@@ -40107,12 +40443,19 @@ var formatInterval = (start2, end) => {
40107
40443
  const endLabel = isSameDay(startDate, endDate) ? formatTimeOfDay(endDate) : formatDateTime(endDate);
40108
40444
  return `${formatDateTime(startDate)} \u2013 ${endLabel}`;
40109
40445
  };
40110
- var SCOPES = ["week", "month", "year", "all"];
40446
+ var SCOPES = ["day", "week", "month", "year", "all"];
40111
40447
  var SCOPE_DURATION_MS = {
40448
+ day: 24 * 60 * 60 * 1e3,
40112
40449
  week: 7 * 24 * 60 * 60 * 1e3,
40113
40450
  month: 30 * 24 * 60 * 60 * 1e3,
40114
40451
  year: 365 * 24 * 60 * 60 * 1e3
40115
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
+ };
40116
40459
  var isScope = (value) => SCOPES.includes(value ?? "");
40117
40460
  var getPeriodRange = (scope, offset) => {
40118
40461
  if (scope === "all") return null;
@@ -40122,9 +40465,7 @@ var getPeriodRange = (scope, offset) => {
40122
40465
  };
40123
40466
  var formatPeriodLabel = (scope, offset, range) => {
40124
40467
  if (scope === "all" || !range) return "All time";
40125
- if (offset === 0) {
40126
- return scope === "week" ? "Last 7 days" : scope === "month" ? "Last 30 days" : "Last 365 days";
40127
- }
40468
+ if (offset === 0) return SCOPE_RECENT_LABELS[scope];
40128
40469
  const start2 = new Date(range.start);
40129
40470
  const end = new Date(range.end);
40130
40471
  return `${start2.getMonth() + 1}/${start2.getDate()} \u2013 ${end.getMonth() + 1}/${end.getDate()}`;
@@ -40141,10 +40482,16 @@ var hashUnitInterval = (key) => {
40141
40482
  var DOT_APPEAR_MS = 260;
40142
40483
  var DOT_APPEAR_SCATTER_MS = 620;
40143
40484
  var dotDelayMs = (key) => Math.round(hashUnitInterval(key) * DOT_APPEAR_SCATTER_MS);
40144
- var dotAppearAnimation = (key) => `epiqScrubberTwinkle ${DOT_APPEAR_MS}ms ease-out ${dotDelayMs(
40145
- key
40146
- )}ms backwards`;
40147
- 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
+ );
40148
40495
  var DOT_EXIT_TOTAL_MS = DOT_APPEAR_SCATTER_MS + DOT_APPEAR_MS;
40149
40496
  var BAR_GROW_MS = 200;
40150
40497
  var BAR_GROW_SWEEP_MS = 560;
@@ -40155,6 +40502,7 @@ var barGrowAnimation = (index2, firstIndex, lastIndex) => {
40155
40502
  0
40156
40503
  )}ms backwards`;
40157
40504
  };
40505
+ var BAR_ENTRANCE_TOTAL_MS = BAR_GROW_MS + BAR_GROW_SWEEP_MS;
40158
40506
  var FADE_IN_ANIMATION = "epiqScrubberFadeIn 320ms ease-out";
40159
40507
  var SCRUBBER_KEYFRAMES = `
40160
40508
  /* Must animate the standalone 'scale' property, not 'transform': the dots
@@ -40185,10 +40533,10 @@ var SCRUBBER_KEYFRAMES = `
40185
40533
  `;
40186
40534
  var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
40187
40535
  var usePrefersReducedMotion = () => {
40188
- const [reduced, setReduced] = (0, import_react12.useState)(
40536
+ const [reduced, setReduced] = (0, import_react13.useState)(
40189
40537
  () => window.matchMedia(REDUCED_MOTION_QUERY).matches
40190
40538
  );
40191
- (0, import_react12.useEffect)(() => {
40539
+ (0, import_react13.useEffect)(() => {
40192
40540
  const query = window.matchMedia(REDUCED_MOTION_QUERY);
40193
40541
  const onChange = () => setReduced(query.matches);
40194
40542
  query.addEventListener("change", onChange);
@@ -40197,12 +40545,12 @@ var usePrefersReducedMotion = () => {
40197
40545
  return reduced;
40198
40546
  };
40199
40547
  var useExitTransition = (visible, durationMs) => {
40200
- const [mounted, setMounted] = (0, import_react12.useState)(visible);
40201
- const [leaving, setLeaving] = (0, import_react12.useState)(false);
40202
- const wasVisible = (0, import_react12.useRef)(visible);
40203
- const duration = (0, import_react12.useRef)(durationMs);
40548
+ const [mounted, setMounted] = (0, import_react13.useState)(visible);
40549
+ const [leaving, setLeaving] = (0, import_react13.useState)(false);
40550
+ const wasVisible = (0, import_react13.useRef)(visible);
40551
+ const duration = (0, import_react13.useRef)(durationMs);
40204
40552
  duration.current = durationMs;
40205
- (0, import_react12.useEffect)(() => {
40553
+ (0, import_react13.useEffect)(() => {
40206
40554
  if (visible === wasVisible.current) return;
40207
40555
  wasVisible.current = visible;
40208
40556
  if (visible) {
@@ -40224,7 +40572,7 @@ var useExitTransition = (visible, durationMs) => {
40224
40572
  return { mounted, leaving };
40225
40573
  };
40226
40574
  var usePersistedFlag = (key, fallback) => {
40227
- const [value, setValue] = (0, import_react12.useState)(() => {
40575
+ const [value, setValue] = (0, import_react13.useState)(() => {
40228
40576
  const stored = localStorage.getItem(key);
40229
40577
  return stored === null ? fallback : stored === "true";
40230
40578
  });
@@ -40236,12 +40584,25 @@ var usePersistedFlag = (key, fallback) => {
40236
40584
  }
40237
40585
  ];
40238
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
+ };
40239
40600
 
40240
40601
  // source/gui/client/components/ScrubberParts.tsx
40241
- var import_react13 = __toESM(require_react(), 1);
40602
+ var import_react14 = __toESM(require_react(), 1);
40242
40603
 
40243
40604
  // source/gui/client/components/Checkbox.tsx
40244
- var import_jsx_runtime26 = __toESM(require_jsx_runtime(), 1);
40605
+ var import_jsx_runtime27 = __toESM(require_jsx_runtime(), 1);
40245
40606
  var Checkbox = ({
40246
40607
  label,
40247
40608
  checked,
@@ -40251,7 +40612,7 @@ var Checkbox = ({
40251
40612
  title
40252
40613
  }) => {
40253
40614
  const color2 = checked ? activeColor : GUI_THEME.dim;
40254
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
40615
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
40255
40616
  "label",
40256
40617
  {
40257
40618
  title,
@@ -40265,7 +40626,7 @@ var Checkbox = ({
40265
40626
  opacity: disabled ? 0.4 : 1
40266
40627
  },
40267
40628
  children: [
40268
- /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
40629
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
40269
40630
  "span",
40270
40631
  {
40271
40632
  style: {
@@ -40282,7 +40643,7 @@ var Checkbox = ({
40282
40643
  flexShrink: 0
40283
40644
  },
40284
40645
  children: [
40285
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
40646
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
40286
40647
  "input",
40287
40648
  {
40288
40649
  type: "checkbox",
@@ -40300,7 +40661,7 @@ var Checkbox = ({
40300
40661
  }
40301
40662
  }
40302
40663
  ),
40303
- checked && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
40664
+ checked && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
40304
40665
  "span",
40305
40666
  {
40306
40667
  style: {
@@ -40321,8 +40682,8 @@ var Checkbox = ({
40321
40682
  };
40322
40683
 
40323
40684
  // source/gui/client/components/IconChevronDown.tsx
40324
- var import_jsx_runtime27 = __toESM(require_jsx_runtime(), 1);
40325
- var IconChevronDown = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
40685
+ var import_jsx_runtime28 = __toESM(require_jsx_runtime(), 1);
40686
+ var IconChevronDown = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
40326
40687
  "svg",
40327
40688
  {
40328
40689
  width: size,
@@ -40334,13 +40695,13 @@ var IconChevronDown = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime2
40334
40695
  strokeLinecap: "round",
40335
40696
  strokeLinejoin: "round",
40336
40697
  "aria-hidden": "true",
40337
- children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("polyline", { points: "6 9 12 15 18 9" })
40698
+ children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("polyline", { points: "6 9 12 15 18 9" })
40338
40699
  }
40339
40700
  );
40340
40701
 
40341
40702
  // source/gui/client/components/IconChevronRight.tsx
40342
- var import_jsx_runtime28 = __toESM(require_jsx_runtime(), 1);
40343
- var IconChevronRight = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
40703
+ var import_jsx_runtime29 = __toESM(require_jsx_runtime(), 1);
40704
+ var IconChevronRight = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40344
40705
  "svg",
40345
40706
  {
40346
40707
  width: size,
@@ -40352,12 +40713,12 @@ var IconChevronRight = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime
40352
40713
  strokeLinecap: "round",
40353
40714
  strokeLinejoin: "round",
40354
40715
  "aria-hidden": "true",
40355
- children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("polyline", { points: "9 18 15 12 9 6" })
40716
+ children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("polyline", { points: "9 18 15 12 9 6" })
40356
40717
  }
40357
40718
  );
40358
40719
 
40359
40720
  // source/gui/client/components/ScrubberParts.tsx
40360
- var import_jsx_runtime29 = __toESM(require_jsx_runtime(), 1);
40721
+ var import_jsx_runtime30 = __toESM(require_jsx_runtime(), 1);
40361
40722
  var toggleButtonStyle = (active) => ({
40362
40723
  background: "transparent",
40363
40724
  border: `1px solid ${active ? GUI_THEME.accent : GUI_THEME.dim}`,
@@ -40367,6 +40728,319 @@ var toggleButtonStyle = (active) => ({
40367
40728
  padding: "2px 8px",
40368
40729
  cursor: "pointer"
40369
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
+ };
40370
41044
  var navButtonStyle = {
40371
41045
  background: "transparent",
40372
41046
  border: `1px solid ${GUI_THEME.dim}`,
@@ -40385,6 +41059,12 @@ var ScrubberControls = ({
40385
41059
  showIssues,
40386
41060
  showCommits,
40387
41061
  allBoards,
41062
+ boardView,
41063
+ identities,
41064
+ hiddenIdentityIds,
41065
+ categoriesExpanded,
41066
+ identitiesExpanded,
41067
+ categoriesFiltered,
40388
41068
  isScrubbing,
40389
41069
  nowLabel,
40390
41070
  onChangeScope,
@@ -40393,11 +41073,16 @@ var ScrubberControls = ({
40393
41073
  onChangeShowIssues,
40394
41074
  onChangeShowCommits,
40395
41075
  onChangeAllBoards,
41076
+ onChangeBoardView,
41077
+ onToggleIdentity,
41078
+ onOnlyIdentity,
41079
+ onToggleCategoriesExpanded,
41080
+ onSetIdentitiesExpanded,
40396
41081
  onReturnToLive
40397
- }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
40398
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
40399
- scope !== "all" && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 2 }, children: [
40400
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41082
+ }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
41083
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
41084
+ scope !== "all" && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 2 }, children: [
41085
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40401
41086
  "button",
40402
41087
  {
40403
41088
  title: "Earlier",
@@ -40406,7 +41091,7 @@ var ScrubberControls = ({
40406
41091
  children: "\u25C0"
40407
41092
  }
40408
41093
  ),
40409
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41094
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40410
41095
  "span",
40411
41096
  {
40412
41097
  style: {
@@ -40423,7 +41108,7 @@ var ScrubberControls = ({
40423
41108
  children: formatPeriodLabel(scope, offset, periodRange)
40424
41109
  }
40425
41110
  ),
40426
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41111
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40427
41112
  "button",
40428
41113
  {
40429
41114
  title: "Later",
@@ -40438,7 +41123,7 @@ var ScrubberControls = ({
40438
41123
  }
40439
41124
  )
40440
41125
  ] }),
40441
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { style: { display: "flex", gap: 2 }, children: SCOPES.map((option) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41126
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { display: "flex", gap: 2 }, children: SCOPES.map((option) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40442
41127
  "button",
40443
41128
  {
40444
41129
  onClick: () => onChangeScope(option),
@@ -40448,8 +41133,8 @@ var ScrubberControls = ({
40448
41133
  option
40449
41134
  )) })
40450
41135
  ] }),
40451
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { style: { display: "flex", gap: 2 }, children: [
40452
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41136
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", gap: 2 }, children: [
41137
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40453
41138
  "button",
40454
41139
  {
40455
41140
  title: "How much happened, per equal-width period \u2014 no empty gaps for quiet stretches",
@@ -40458,7 +41143,7 @@ var ScrubberControls = ({
40458
41143
  children: "Volume"
40459
41144
  }
40460
41145
  ),
40461
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41146
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40462
41147
  "button",
40463
41148
  {
40464
41149
  title: "Individual events by exact moment \u2014 x is elapsed time, y is time of day",
@@ -40468,17 +41153,8 @@ var ScrubberControls = ({
40468
41153
  }
40469
41154
  )
40470
41155
  ] }),
40471
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { style: { display: "flex", gap: 10 }, children: [
40472
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40473
- Checkbox,
40474
- {
40475
- label: "Board",
40476
- checked: showIssues,
40477
- activeColor: GUI_THEME.accent,
40478
- onChange: onChangeShowIssues
40479
- }
40480
- ),
40481
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41156
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)("div", { style: { display: "flex", gap: 12, alignItems: "center" }, children: [
41157
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40482
41158
  Checkbox,
40483
41159
  {
40484
41160
  label: "Code",
@@ -40487,17 +41163,26 @@ var ScrubberControls = ({
40487
41163
  onChange: onChangeShowCommits
40488
41164
  }
40489
41165
  ),
40490
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40491
- Checkbox,
41166
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41167
+ BoardSeriesGroup,
40492
41168
  {
40493
- label: "All boards",
40494
- checked: allBoards,
40495
- activeColor: GUI_THEME.accent,
40496
- 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
40497
41182
  }
40498
41183
  )
40499
41184
  ] }),
40500
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41185
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40501
41186
  "div",
40502
41187
  {
40503
41188
  style: {
@@ -40509,7 +41194,7 @@ var ScrubberControls = ({
40509
41194
  width: 100,
40510
41195
  flexShrink: 0
40511
41196
  },
40512
- children: isScrubbing ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41197
+ children: isScrubbing ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40513
41198
  "button",
40514
41199
  {
40515
41200
  onClick: onReturnToLive,
@@ -40525,7 +41210,7 @@ var ScrubberControls = ({
40525
41210
  },
40526
41211
  children: "Return to live"
40527
41212
  }
40528
- ) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41213
+ ) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40529
41214
  "span",
40530
41215
  {
40531
41216
  style: {
@@ -40545,7 +41230,7 @@ var ScrubberHeader = ({
40545
41230
  collapsed,
40546
41231
  onToggleCollapsed,
40547
41232
  scrubbingAsOf
40548
- }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
41233
+ }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40549
41234
  "div",
40550
41235
  {
40551
41236
  style: {
@@ -40556,7 +41241,7 @@ var ScrubberHeader = ({
40556
41241
  whiteSpace: "nowrap"
40557
41242
  },
40558
41243
  children: [
40559
- /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
41244
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40560
41245
  "button",
40561
41246
  {
40562
41247
  onClick: onToggleCollapsed,
@@ -40574,13 +41259,13 @@ var ScrubberHeader = ({
40574
41259
  },
40575
41260
  children: [
40576
41261
  "Time travel",
40577
- collapsed ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(IconChevronRight, { size: 12 }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(IconChevronDown, { size: 12 })
41262
+ collapsed ? /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(IconChevronRight, { size: 12 }) : /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(IconChevronDown, { size: 12 })
40578
41263
  ]
40579
41264
  }
40580
41265
  ),
40581
- scrubbingAsOf !== null && /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
40582
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { style: { color: GUI_THEME.accent, fontWeight: 700 }, children: "Read-only" }),
40583
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("span", { style: { color: GUI_THEME.primary }, children: scrubbingAsOf })
41266
+ scrubbingAsOf !== null && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
41267
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { style: { color: GUI_THEME.accent, fontWeight: 700 }, children: "Read-only" }),
41268
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("span", { style: { color: GUI_THEME.primary }, children: scrubbingAsOf })
40584
41269
  ] })
40585
41270
  ]
40586
41271
  }
@@ -40590,7 +41275,7 @@ var SegmentHighlight = ({
40590
41275
  fractionForTime
40591
41276
  }) => {
40592
41277
  const left = fractionForTime(segment.start);
40593
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41278
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40594
41279
  "div",
40595
41280
  {
40596
41281
  style: {
@@ -40622,7 +41307,7 @@ var SegmentHighlight = ({
40622
41307
  var TrackBaseline = ({
40623
41308
  color: color2,
40624
41309
  anchor
40625
- }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41310
+ }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40626
41311
  "div",
40627
41312
  {
40628
41313
  style: {
@@ -40638,11 +41323,11 @@ var TrackBaseline = ({
40638
41323
  }
40639
41324
  }
40640
41325
  );
40641
- var HourAxisLabels = () => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_jsx_runtime29.Fragment, { children: [
41326
+ var HourAxisLabels = () => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(import_jsx_runtime30.Fragment, { children: [
40642
41327
  ["00:00", { top: EVENTS_MODE_VERTICAL_PADDING }],
40643
41328
  ["12:00", { top: "50%", transform: "translateY(-50%)" }],
40644
41329
  ["24:00", { bottom: EVENTS_MODE_VERTICAL_PADDING }]
40645
- ].map(([label, position3]) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41330
+ ].map(([label, position3]) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40646
41331
  "span",
40647
41332
  {
40648
41333
  style: {
@@ -40660,14 +41345,18 @@ var HourAxisLabels = () => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_
40660
41345
  var SeriesLayer = ({
40661
41346
  animate,
40662
41347
  children
40663
- }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41348
+ }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40664
41349
  "div",
40665
41350
  {
40666
41351
  style: {
40667
41352
  position: "absolute",
40668
41353
  inset: 0,
40669
41354
  pointerEvents: "none",
40670
- 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"
40671
41360
  },
40672
41361
  children
40673
41362
  }
@@ -40685,8 +41374,13 @@ var VolumeBars = ({
40685
41374
  const widthPercent = 100 / bucketCount;
40686
41375
  const barWidth = barWidthCss(bucketCount);
40687
41376
  const growsUp = direction === "up";
40688
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
40689
- highlightedIndex !== null && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41377
+ const [fresh, setFresh] = (0, import_react14.useState)(true);
41378
+ (0, import_react14.useEffect)(() => {
41379
+ const timeout = setTimeout(() => setFresh(false), BAR_ENTRANCE_TOTAL_MS);
41380
+ return () => clearTimeout(timeout);
41381
+ }, []);
41382
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
41383
+ highlightedIndex !== null && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40690
41384
  "div",
40691
41385
  {
40692
41386
  style: {
@@ -40702,7 +41396,7 @@ var VolumeBars = ({
40702
41396
  }
40703
41397
  }
40704
41398
  ),
40705
- bars.map(({ index: index2, intensity }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41399
+ bars.map(({ index: index2, intensity }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40706
41400
  "div",
40707
41401
  {
40708
41402
  style: {
@@ -40715,7 +41409,7 @@ var VolumeBars = ({
40715
41409
  background: color2,
40716
41410
  opacity: 0.35 + intensity * 0.65,
40717
41411
  transformOrigin: growsUp ? "bottom" : "top",
40718
- animation: animate ? barGrowAnimation(index2, firstBar, lastBar) : void 0,
41412
+ animation: animate && fresh ? barGrowAnimation(index2, firstBar, lastBar) : void 0,
40719
41413
  pointerEvents: "none"
40720
41414
  }
40721
41415
  },
@@ -40723,55 +41417,14 @@ var VolumeBars = ({
40723
41417
  ))
40724
41418
  ] });
40725
41419
  };
40726
- var ScatterDot = ({
40727
- fraction,
40728
- hourFraction,
40729
- size,
40730
- color: color2,
40731
- opacity,
40732
- zIndex,
40733
- title,
40734
- animation,
40735
- interactive = true,
40736
- onMouseEnter,
40737
- onMouseLeave,
40738
- onClick
40739
- }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40740
- "div",
40741
- {
40742
- title,
40743
- onPointerDown: onClick && ((event) => event.stopPropagation()),
40744
- onClick: onClick && ((event) => {
40745
- event.stopPropagation();
40746
- onClick();
40747
- }),
40748
- onMouseEnter,
40749
- onMouseLeave,
40750
- style: {
40751
- position: "absolute",
40752
- left: `${fraction * 100}%`,
40753
- top: EVENTS_MODE_VERTICAL_PADDING + hourFraction * EVENTS_SCATTER_HEIGHT,
40754
- width: size,
40755
- height: size,
40756
- borderRadius: "50%",
40757
- background: color2,
40758
- opacity,
40759
- zIndex,
40760
- transform: `translate(${-size / 2}px, -50%)`,
40761
- animation,
40762
- pointerEvents: interactive ? "auto" : "none",
40763
- cursor: onClick ? "pointer" : void 0
40764
- }
40765
- }
40766
- );
40767
41420
  var ScrubberNeedle = ({ fraction }) => {
40768
- const [hovered, setHovered] = (0, import_react13.useState)(false);
41421
+ const [hovered, setHovered] = (0, import_react14.useState)(false);
40769
41422
  const hover = {
40770
41423
  onMouseEnter: () => setHovered(true),
40771
41424
  onMouseLeave: () => setHovered(false)
40772
41425
  };
40773
- return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
40774
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41426
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(import_jsx_runtime30.Fragment, { children: [
41427
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40775
41428
  "div",
40776
41429
  {
40777
41430
  ...hover,
@@ -40791,7 +41444,7 @@ var ScrubberNeedle = ({ fraction }) => {
40791
41444
  }
40792
41445
  }
40793
41446
  ),
40794
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41447
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40795
41448
  "div",
40796
41449
  {
40797
41450
  ...hover,
@@ -40821,7 +41474,7 @@ var ScrubberHoverHint = ({
40821
41474
  fraction,
40822
41475
  trackWidthPx,
40823
41476
  empty: empty3 = false
40824
- }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
41477
+ }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40825
41478
  "div",
40826
41479
  {
40827
41480
  style: {
@@ -40853,8 +41506,8 @@ var ScrubberHoverHint = ({
40853
41506
  zIndex: 5
40854
41507
  },
40855
41508
  children: [
40856
- segmentLabel && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { style: { fontSize: 10, color: GUI_THEME.dim }, children: segmentLabel }),
40857
- /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41509
+ segmentLabel && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("div", { style: { fontSize: 10, color: GUI_THEME.dim }, children: segmentLabel }),
41510
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40858
41511
  "div",
40859
41512
  {
40860
41513
  style: {
@@ -40867,7 +41520,7 @@ var ScrubberHoverHint = ({
40867
41520
  children: label
40868
41521
  }
40869
41522
  ),
40870
- rows.map((row, index2) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41523
+ rows.map((row, index2) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40871
41524
  "div",
40872
41525
  {
40873
41526
  style: {
@@ -40883,10 +41536,175 @@ var ScrubberHoverHint = ({
40883
41536
  ]
40884
41537
  }
40885
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
+ };
40886
41705
 
40887
41706
  // source/gui/client/components/ScrubberLayout.tsx
40888
- var import_jsx_runtime30 = __toESM(require_jsx_runtime(), 1);
40889
- var dotAnimation = (key, animate, leaving) => !animate ? void 0 : leaving ? dotExitAnimation(key) : dotAppearAnimation(key);
41707
+ var import_jsx_runtime31 = __toESM(require_jsx_runtime(), 1);
40890
41708
  var ScrubberLayout = ({
40891
41709
  collapsed,
40892
41710
  onToggleCollapsed,
@@ -40895,7 +41713,7 @@ var ScrubberLayout = ({
40895
41713
  chart
40896
41714
  }) => {
40897
41715
  const { axis, layoutMode, animate, windowKey, on } = chart;
40898
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
41716
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
40899
41717
  Panel,
40900
41718
  {
40901
41719
  as: "div",
@@ -40912,8 +41730,8 @@ var ScrubberLayout = ({
40912
41730
  overflow: "visible"
40913
41731
  },
40914
41732
  children: [
40915
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("style", { children: SCRUBBER_KEYFRAMES }),
40916
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
41733
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("style", { children: SCRUBBER_KEYFRAMES }),
41734
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
40917
41735
  "div",
40918
41736
  {
40919
41737
  style: {
@@ -40922,7 +41740,7 @@ var ScrubberLayout = ({
40922
41740
  gap: collapsed ? 0 : 8
40923
41741
  },
40924
41742
  children: [
40925
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
41743
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
40926
41744
  "div",
40927
41745
  {
40928
41746
  style: {
@@ -40934,7 +41752,7 @@ var ScrubberLayout = ({
40934
41752
  minHeight: 22
40935
41753
  },
40936
41754
  children: [
40937
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41755
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
40938
41756
  ScrubberHeader,
40939
41757
  {
40940
41758
  collapsed,
@@ -40942,7 +41760,7 @@ var ScrubberLayout = ({
40942
41760
  scrubbingAsOf
40943
41761
  }
40944
41762
  ),
40945
- !collapsed && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ScrubberControls, { ...controls })
41763
+ !collapsed && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ScrubberControls, { ...controls })
40946
41764
  ]
40947
41765
  }
40948
41766
  ),
@@ -40950,7 +41768,7 @@ var ScrubberLayout = ({
40950
41768
  // spanning them and the gap between. Pointer handlers belong here
40951
41769
  // rather than on either chart, so a drag or hover anywhere across
40952
41770
  // the pair — the gap included — counts as one timeline.
40953
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
41771
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
40954
41772
  "div",
40955
41773
  {
40956
41774
  onPointerDown: on.onPointerDown,
@@ -40967,14 +41785,14 @@ var ScrubberLayout = ({
40967
41785
  cursor: "pointer"
40968
41786
  },
40969
41787
  children: [
40970
- chart.hoveredSegment && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41788
+ chart.hoveredSegment && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
40971
41789
  SegmentHighlight,
40972
41790
  {
40973
41791
  segment: chart.hoveredSegment,
40974
41792
  fractionForTime: axis.fractionForTime
40975
41793
  }
40976
41794
  ),
40977
- /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
41795
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
40978
41796
  "div",
40979
41797
  {
40980
41798
  ref: chart.trackRef,
@@ -40989,15 +41807,15 @@ var ScrubberLayout = ({
40989
41807
  alignItems: "center"
40990
41808
  },
40991
41809
  children: [
40992
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41810
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
40993
41811
  TrackBaseline,
40994
41812
  {
40995
- color: GUI_THEME.accent,
41813
+ color: chart.issueSeriesColor,
40996
41814
  anchor: layoutMode === "even" ? "bottom" : "centre"
40997
41815
  }
40998
41816
  ),
40999
- layoutMode === "real" && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(HourAxisLabels, {}),
41000
- chart.showIssues && layoutMode === "even" && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(SeriesLayer, { animate, children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41817
+ layoutMode === "real" && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(HourAxisLabels, {}),
41818
+ chart.showIssues && layoutMode === "even" && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(SeriesLayer, { animate, children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41001
41819
  VolumeBars,
41002
41820
  {
41003
41821
  bars: chart.issueBars,
@@ -41005,75 +41823,25 @@ var ScrubberLayout = ({
41005
41823
  firstBar: chart.issueBarRange[0],
41006
41824
  lastBar: chart.issueBarRange[1],
41007
41825
  highlightedIndex: chart.hoveredBucketIndex,
41008
- color: GUI_THEME.accent,
41826
+ color: chart.issueSeriesColor,
41009
41827
  direction: "up",
41010
41828
  animate
41011
41829
  }
41012
41830
  ) }, `issues-${windowKey}`),
41013
- chart.issueScatter.mounted && layoutMode === "real" && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41014
- SeriesLayer,
41015
- {
41016
- animate: animate && !chart.issueScatter.leaving,
41017
- children: chart.eventBuckets.map((bucket) => {
41018
- const intensity = bucket.count / chart.maxEventCount;
41019
- return /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41020
- ScatterDot,
41021
- {
41022
- fraction: axis.fractionForTime(bucket.t),
41023
- hourFraction: hourFractionForTime(bucket.t),
41024
- size: 3 + intensity * 6,
41025
- color: GUI_THEME.accent,
41026
- opacity: 0.3 + intensity * 0.5,
41027
- zIndex: 2,
41028
- title: `${bucket.count} change${bucket.count === 1 ? "" : "s"}, ${formatDateTime(new Date(bucket.t))}`,
41029
- animation: dotAnimation(
41030
- String(bucket.t),
41031
- animate,
41032
- chart.issueScatter.leaving
41033
- ),
41034
- interactive: !chart.issueScatter.leaving,
41035
- onMouseEnter: () => on.onIssueDotEnter(bucket),
41036
- onMouseLeave: on.onIssueDotLeave
41037
- },
41038
- bucket.t
41039
- );
41040
- })
41041
- },
41042
- `issues-${windowKey}`
41043
- ),
41044
- chart.commitScatter.mounted && layoutMode === "real" && chart.commits.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41045
- SeriesLayer,
41831
+ layoutMode === "real" && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41832
+ ScatterCanvas,
41046
41833
  {
41047
- animate: animate && !chart.commitScatter.leaving,
41048
- children: chart.commits.map((commit) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41049
- ScatterDot,
41050
- {
41051
- fraction: axis.fractionForTime(commit.time),
41052
- hourFraction: hourFractionForTime(commit.time),
41053
- size: 4,
41054
- color: GUI_THEME.green,
41055
- opacity: chart.hoveredCommitSha === commit.sha ? 1 : 0.55,
41056
- zIndex: 1,
41057
- title: `${formatDateTime(new Date(commit.time))} \u2014 ${commit.subject} \u2014 ${commit.author} (${commit.linesChanged.toLocaleString()} lines)`,
41058
- animation: dotAnimation(
41059
- commit.sha,
41060
- animate,
41061
- chart.commitScatter.leaving
41062
- ),
41063
- interactive: !chart.commitScatter.leaving,
41064
- onClick: () => on.onInspectCommit(commit.sha),
41065
- onMouseEnter: () => on.onCommitDotEnter(commit),
41066
- onMouseLeave: on.onCommitDotLeave
41067
- },
41068
- commit.sha
41069
- ))
41070
- },
41071
- `commits-${windowKey}`
41834
+ layers: chart.scatterLayers,
41835
+ animate,
41836
+ onPointEnter: on.onScatterPointEnter,
41837
+ onPointLeave: on.onScatterPointLeave,
41838
+ onInspectCommit: on.onInspectCommit
41839
+ }
41072
41840
  )
41073
41841
  ]
41074
41842
  }
41075
41843
  ),
41076
- chart.showCommits && layoutMode === "even" && chart.commits.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
41844
+ chart.showCommits && layoutMode === "even" && chart.commits.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
41077
41845
  "div",
41078
41846
  {
41079
41847
  onMouseEnter: on.onCommitTrackMouseEnter,
@@ -41086,8 +41854,8 @@ var ScrubberLayout = ({
41086
41854
  animation: animate ? FADE_IN_ANIMATION : void 0
41087
41855
  },
41088
41856
  children: [
41089
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(TrackBaseline, { color: GUI_THEME.green, anchor: "top" }),
41090
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41857
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(TrackBaseline, { color: GUI_THEME.green, anchor: "top" }),
41858
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41091
41859
  VolumeBars,
41092
41860
  {
41093
41861
  bars: chart.commitBars,
@@ -41104,17 +41872,17 @@ var ScrubberLayout = ({
41104
41872
  },
41105
41873
  `commits-${windowKey}`
41106
41874
  ),
41107
- /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ScrubberNeedle, { fraction: chart.thumbFraction }),
41108
- chart.boardHint && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41875
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(ScrubberNeedle, { fraction: chart.thumbFraction }),
41876
+ chart.boardHint && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41109
41877
  ScrubberHoverHint,
41110
41878
  {
41111
41879
  ...chart.boardHint,
41112
41880
  segmentLabel: chart.hoveredSegment?.label,
41113
- stripeColor: GUI_THEME.accent,
41881
+ stripeColor: chart.issueSeriesColor,
41114
41882
  trackWidthPx: chart.trackWidthPx
41115
41883
  }
41116
41884
  ),
41117
- chart.commitHint && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41885
+ chart.commitHint && /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41118
41886
  ScrubberHoverHint,
41119
41887
  {
41120
41888
  ...chart.commitHint,
@@ -41135,13 +41903,31 @@ var ScrubberLayout = ({
41135
41903
  };
41136
41904
 
41137
41905
  // source/gui/client/components/TimeScrubber.tsx
41138
- var import_jsx_runtime31 = __toESM(require_jsx_runtime(), 1);
41906
+ var import_jsx_runtime32 = __toESM(require_jsx_runtime(), 1);
41139
41907
  var SCRUB_THROTTLE_MS = 120;
41908
+ var boardEventRow = (count) => `${count} board event${count === 1 ? "" : "s"}`;
41140
41909
  var COLLAPSED_STORAGE_KEY = "epiq.timeScrubber.collapsed";
41141
41910
  var SCOPE_STORAGE_KEY = "epiq.timeScrubber.scope";
41142
41911
  var SHOW_ISSUES_STORAGE_KEY = "epiq.timeScrubber.showIssues";
41143
41912
  var SHOW_COMMITS_STORAGE_KEY = "epiq.timeScrubber.showCommits";
41144
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
+ };
41145
41931
  var readStoredScope = () => {
41146
41932
  const stored = localStorage.getItem(SCOPE_STORAGE_KEY);
41147
41933
  return isScope(stored) ? stored : "all";
@@ -41149,21 +41935,23 @@ var readStoredScope = () => {
41149
41935
  var TimeScrubber = ({
41150
41936
  timeline,
41151
41937
  commits,
41938
+ historyId,
41152
41939
  timeTravel,
41153
41940
  onScrub,
41154
41941
  onReturnToLive,
41155
41942
  onRequestHistory,
41156
41943
  boardId,
41157
41944
  connected,
41158
- onInspectCommit
41945
+ onInspectCommit,
41946
+ onBoardFilterChange
41159
41947
  }) => {
41160
41948
  const animate = !usePrefersReducedMotion();
41161
- const trackRef = (0, import_react14.useRef)(null);
41162
- const lastDispatchRef = (0, import_react14.useRef)(0);
41163
- const [layoutMode, setLayoutMode] = (0, import_react14.useState)("even");
41164
- const [scope, setScope] = (0, import_react14.useState)(readStoredScope);
41165
- const [offset, setOffset] = (0, import_react14.useState)(0);
41166
- const [dragFraction, setDragFraction] = (0, import_react14.useState)(null);
41949
+ const trackRef = (0, import_react15.useRef)(null);
41950
+ const lastDispatchRef = (0, import_react15.useRef)(0);
41951
+ const [layoutMode, setLayoutMode] = (0, import_react15.useState)("even");
41952
+ const [scope, setScope] = (0, import_react15.useState)(readStoredScope);
41953
+ const [offset, setOffset] = (0, import_react15.useState)(0);
41954
+ const [dragFraction, setDragFraction] = (0, import_react15.useState)(null);
41167
41955
  const [collapsed, setCollapsed] = usePersistedFlag(
41168
41956
  COLLAPSED_STORAGE_KEY,
41169
41957
  false
@@ -41176,6 +41964,16 @@ var TimeScrubber = ({
41176
41964
  SHOW_COMMITS_STORAGE_KEY,
41177
41965
  true
41178
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
+ );
41179
41977
  const [allBoards, setAllBoards] = usePersistedFlag(
41180
41978
  ALL_BOARDS_STORAGE_KEY,
41181
41979
  false
@@ -41183,32 +41981,146 @@ var TimeScrubber = ({
41183
41981
  const exitMs = layoutMode === "real" && animate ? DOT_EXIT_TOTAL_MS : 0;
41184
41982
  const issueScatter = useExitTransition(showIssues, exitMs);
41185
41983
  const commitScatter = useExitTransition(showCommits, exitMs);
41186
- const [hoveredEvent, setHoveredEvent] = (0, import_react14.useState)(null);
41187
- const [pointerFraction, setPointerFraction] = (0, import_react14.useState)(null);
41188
- const [hoveredCommit, setHoveredCommit] = (0, import_react14.useState)(null);
41189
- const [hoveredBucketIndex, setHoveredBucketIndex] = (0, import_react14.useState)(
41984
+ const [hoveredEvent, setHoveredEvent] = (0, import_react15.useState)(null);
41985
+ const [pointerFraction, setPointerFraction] = (0, import_react15.useState)(null);
41986
+ const [hoveredCommit, setHoveredCommit] = (0, import_react15.useState)(null);
41987
+ const [hoveredBucketIndex, setHoveredBucketIndex] = (0, import_react15.useState)(
41190
41988
  null
41191
41989
  );
41192
- const [hoveredCommitBucketIndex, setHoveredCommitBucketIndex] = (0, import_react14.useState)(null);
41990
+ const [hoveredCommitBucketIndex, setHoveredCommitBucketIndex] = (0, import_react15.useState)(null);
41193
41991
  const periodRange = getPeriodRange(scope, offset);
41194
- (0, import_react14.useEffect)(() => {
41992
+ (0, import_react15.useEffect)(() => {
41195
41993
  if (!connected) return;
41196
41994
  onRequestHistory(periodRange?.start, periodRange?.end, allBoards);
41197
41995
  }, [scope, offset, boardId, allBoards, connected]);
41198
41996
  const changeScope = (nextScope) => {
41997
+ armEntrance();
41199
41998
  setScope(nextScope);
41200
41999
  setOffset(0);
41201
42000
  localStorage.setItem(SCOPE_STORAGE_KEY, nextScope);
41202
42001
  };
41203
- const axis = buildAxis(timeline, commits);
41204
- const issueCounts = bucketIssueCounts(axis, timeline);
41205
- const commitStats = bucketCommitStats(axis, commits);
41206
- const eventBuckets = timeline?.buckets ?? [];
41207
- const maxIssueBucketCount = maxOf(issueCounts, 1);
41208
- const maxEventCount = maxOf(
41209
- eventBuckets.map((bucket) => bucket.count),
41210
- 1
42002
+ const changeOffset = (nextOffset) => {
42003
+ armEntrance();
42004
+ setOffset(nextOffset);
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
+ };
42037
+ const changeAllBoards = (next) => {
42038
+ armEntrance();
42039
+ setAllBoards(next);
42040
+ };
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]
41211
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]
42119
+ );
42120
+ (0, import_react15.useEffect)(() => {
42121
+ onBoardFilterChange(boardFilter);
42122
+ }, [boardFilter]);
42123
+ const maxIssueBucketCount = maxOf(issueCounts, 1);
41212
42124
  const maxCommitCount = maxOf(
41213
42125
  Array.from(commitStats.values(), (stats) => stats.count),
41214
42126
  1
@@ -41220,11 +42132,47 @@ var TimeScrubber = ({
41220
42132
  index: index2,
41221
42133
  intensity: stats.count / maxCommitCount
41222
42134
  }));
41223
- const [animationGeneration, setAnimationGeneration] = (0, import_react14.useState)(0);
41224
- const hasData = timeline !== null;
41225
- (0, import_react14.useEffect)(() => {
41226
- setAnimationGeneration((generation) => generation + 1);
41227
- }, [scope, offset, boardId, allBoards, layoutMode, hasData]);
42135
+ const [entrance, setEntrance] = (0, import_react15.useState)(0);
42136
+ const armed = (0, import_react15.useRef)(true);
42137
+ const seenHistoryId = (0, import_react15.useRef)(historyId);
42138
+ const armEntrance = () => {
42139
+ armed.current = true;
42140
+ };
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]);
42147
+ (0, import_react15.useEffect)(() => {
42148
+ if (seenHistoryId.current === historyId) return;
42149
+ seenHistoryId.current = historyId;
42150
+ if (!armed.current) return;
42151
+ armed.current = false;
42152
+ setEntrance((generation) => generation + 1);
42153
+ }, [historyId]);
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
+ );
41228
42176
  const confirmedFraction = timeTravel.mode === "scrub" && timeTravel.asOfTime !== null ? axis.fractionForTime(timeTravel.asOfTime) : 1;
41229
42177
  const fractionFromClientX = (clientX) => {
41230
42178
  const track = trackRef.current;
@@ -41254,7 +42202,30 @@ var TimeScrubber = ({
41254
42202
  );
41255
42203
  const bucketTimeAt = (index2) => index2 !== null && index2 < axis.bucketCount ? axis.bucketTimeAt(index2) : void 0;
41256
42204
  const centreFraction = (index2) => (index2 + 0.5) / axis.bucketCount;
41257
- 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
+ }, []);
41258
42229
  const hoveredBucketTime = bucketTimeAt(hoveredBucketIndex);
41259
42230
  const hoveredBucketCount = hoveredBucketIndex !== null ? issueCounts[hoveredBucketIndex] : void 0;
41260
42231
  const boardHint = layoutMode === "even" ? hoveredBucketTime !== void 0 && hoveredBucketCount !== void 0 ? {
@@ -41267,7 +42238,7 @@ var TimeScrubber = ({
41267
42238
  empty: hoveredBucketCount === 0
41268
42239
  } : null : hoveredEvent ? {
41269
42240
  label: hoveredEvent.label,
41270
- rows: [boardEventRow(hoveredEvent.count)],
42241
+ rows: [hoveredEvent.detail],
41271
42242
  fraction: hoveredEvent.fraction
41272
42243
  } : null;
41273
42244
  const hoveredCommitStats = hoveredCommitBucketIndex !== null ? commitStats.get(hoveredCommitBucketIndex) : void 0;
@@ -41296,7 +42267,7 @@ var TimeScrubber = ({
41296
42267
  hoveredEvent?.t ?? hoveredCommit?.commit.time ?? (pointerFraction !== null ? axis.fractionToTime(pointerFraction) : void 0)
41297
42268
  )) ?? null;
41298
42269
  const segmentUnit = chooseSegmentUnit(axis.span);
41299
- return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
42270
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41300
42271
  ScrubberLayout,
41301
42272
  {
41302
42273
  collapsed,
@@ -41313,11 +42284,22 @@ var TimeScrubber = ({
41313
42284
  isScrubbing: timeTravel.mode === "scrub",
41314
42285
  nowLabel: scope === "all" || offset === 0 ? "Now" : formatDateTime(new Date(axis.latest)),
41315
42286
  onChangeScope: changeScope,
41316
- onChangeOffset: setOffset,
42287
+ onChangeOffset: changeOffset,
41317
42288
  onChangeLayoutMode: setLayoutMode,
41318
42289
  onChangeShowIssues: setShowIssues,
41319
42290
  onChangeShowCommits: setShowCommits,
41320
- onChangeAllBoards: setAllBoards,
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,
41321
42303
  onReturnToLive
41322
42304
  },
41323
42305
  chart: {
@@ -41325,7 +42307,7 @@ var TimeScrubber = ({
41325
42307
  axis,
41326
42308
  layoutMode,
41327
42309
  animate,
41328
- windowKey: `${layoutMode}-${animationGeneration}`,
42310
+ windowKey,
41329
42311
  showIssues,
41330
42312
  showCommits,
41331
42313
  issueScatter,
@@ -41334,9 +42316,12 @@ var TimeScrubber = ({
41334
42316
  issueBarRange: populatedRange(issueBars),
41335
42317
  commitBars,
41336
42318
  commitBarRange: populatedRange(commitBars),
41337
- eventBuckets,
41338
- maxEventCount,
41339
- 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,
41340
42325
  hoveredCommitSha: hoveredCommit?.commit.sha ?? null,
41341
42326
  hoveredBucketIndex,
41342
42327
  hoveredCommitBucketIndex,
@@ -41376,18 +42361,8 @@ var TimeScrubber = ({
41376
42361
  setHoveredCommitBucketIndex(bucketIndexFromEvent(event));
41377
42362
  },
41378
42363
  onCommitTrackMouseLeave: () => setHoveredCommitBucketIndex(null),
41379
- onIssueDotEnter: (bucket) => setHoveredEvent({
41380
- label: formatDateTime(new Date(bucket.t)),
41381
- count: bucket.count,
41382
- t: bucket.t,
41383
- fraction: axis.fractionForTime(bucket.t)
41384
- }),
41385
- onIssueDotLeave: () => setHoveredEvent(null),
41386
- onCommitDotEnter: (commit) => setHoveredCommit({
41387
- commit,
41388
- fraction: axis.fractionForTime(commit.time)
41389
- }),
41390
- onCommitDotLeave: () => setHoveredCommit(null),
42364
+ onScatterPointEnter,
42365
+ onScatterPointLeave,
41391
42366
  onInspectCommit
41392
42367
  }
41393
42368
  }
@@ -41395,34 +42370,6 @@ var TimeScrubber = ({
41395
42370
  );
41396
42371
  };
41397
42372
 
41398
- // source/gui/client/lib/socket-send.ts
41399
- var pending = /* @__PURE__ */ new WeakMap();
41400
- var sendSocketJson = (socket, data2) => {
41401
- if (!socket) return;
41402
- const message = JSON.stringify(data2);
41403
- if (socket.readyState === WebSocket.OPEN) {
41404
- socket.send(message);
41405
- return;
41406
- }
41407
- if (socket.readyState !== WebSocket.CONNECTING) return;
41408
- const queued = pending.get(socket);
41409
- if (queued) {
41410
- queued.push(message);
41411
- return;
41412
- }
41413
- pending.set(socket, [message]);
41414
- socket.addEventListener(
41415
- "open",
41416
- () => {
41417
- for (const queuedMessage of pending.get(socket) ?? []) {
41418
- socket.send(queuedMessage);
41419
- }
41420
- pending.delete(socket);
41421
- },
41422
- { once: true }
41423
- );
41424
- };
41425
-
41426
42373
  // source/gui/client/lib/gui-move-issue.tsx
41427
42374
  var toMovePosition = (targetIssues, targetIndex) => {
41428
42375
  if (targetIssues.length === 0) return { at: "start" };
@@ -41467,47 +42414,36 @@ var findMove = (state, issueId, parentId, targetIndex) => {
41467
42414
  targetParentId: targetSwimlane.id
41468
42415
  };
41469
42416
  };
41470
- var moveIssue = (setState, socketRef) => (issueId, parentId, targetIndex) => {
41471
- let position3 = null;
41472
- setState((current) => {
41473
- if (!current) return current;
41474
- const move = findMove(current, issueId, parentId, targetIndex);
41475
- if (!move) return current;
41476
- position3 = move.position;
41477
- return {
41478
- ...current,
41479
- boards: current.boards.map((board) => ({
41480
- ...board,
41481
- swimlanes: board.swimlanes.map((swimlane) => {
41482
- const issuesWithoutMoved = swimlane.issues.filter(
41483
- (issue) => issue.id !== issueId
41484
- );
41485
- if (swimlane.id !== parentId) {
41486
- return {
41487
- ...swimlane,
41488
- issues: issuesWithoutMoved
41489
- };
41490
- }
41491
- const nextIssues = [...issuesWithoutMoved];
41492
- const index2 = targetIndex === "end" ? nextIssues.length : Math.max(0, Math.min(targetIndex, nextIssues.length));
41493
- nextIssues.splice(index2, 0, move.movedIssue);
41494
- return {
41495
- ...swimlane,
41496
- issues: nextIssues
41497
- };
41498
- })
41499
- }))
41500
- };
41501
- });
41502
- if (!position3) return;
41503
- sendSocketJson(socketRef.current, {
41504
- type: "issues:move",
41505
- payload: {
41506
- issueId,
41507
- parentId,
41508
- position: position3
41509
- }
41510
- });
42417
+ var applyMove = (state, issueId, parentId, targetIndex, movedIssue) => ({
42418
+ ...state,
42419
+ boards: state.boards.map((board) => ({
42420
+ ...board,
42421
+ swimlanes: board.swimlanes.map((swimlane) => {
42422
+ const issuesWithoutMoved = swimlane.issues.filter(
42423
+ (issue) => issue.id !== issueId
42424
+ );
42425
+ if (swimlane.id !== parentId) {
42426
+ return { ...swimlane, issues: issuesWithoutMoved };
42427
+ }
42428
+ const nextIssues = [...issuesWithoutMoved];
42429
+ const index2 = targetIndex === "end" ? nextIssues.length : Math.max(0, Math.min(targetIndex, nextIssues.length));
42430
+ nextIssues.splice(index2, 0, movedIssue);
42431
+ return { ...swimlane, issues: nextIssues };
42432
+ })
42433
+ }))
42434
+ });
42435
+ var moveIssue = (state, setState, send) => (issueIds, parentId, targetIndex) => {
42436
+ if (!state) return;
42437
+ let running = state;
42438
+ for (const [offset, issueId] of issueIds.entries()) {
42439
+ const index2 = targetIndex === "end" ? "end" : targetIndex + offset;
42440
+ const move = findMove(running, issueId, parentId, index2);
42441
+ if (!move) continue;
42442
+ running = applyMove(running, issueId, parentId, index2, move.movedIssue);
42443
+ send("issues:move", { issueId, parentId, position: move.position });
42444
+ }
42445
+ const next = running;
42446
+ setState((current) => current ? next : current);
41511
42447
  };
41512
42448
 
41513
42449
  // source/lib/utils/node-ref.ts
@@ -41517,8 +42453,8 @@ var nodeRef = (id) => id.slice(-NODE_REF_LENGTH).toUpperCase();
41517
42453
  // source/gui/client/lib/gui-state-helper.tsx
41518
42454
  var getResultValue = (payload) => {
41519
42455
  if (!payload) return void 0;
41520
- if (typeof payload === "object" && payload !== null && "value" in payload && payload.value) {
41521
- return payload.value;
42456
+ if (typeof payload === "object" && payload !== null && "value" in payload) {
42457
+ return payload.value ?? void 0;
41522
42458
  }
41523
42459
  if (typeof payload === "object" && payload !== null && "content" in payload && payload.content?.[0]?.text) {
41524
42460
  return JSON.parse(payload.content[0].text).value;
@@ -41552,6 +42488,34 @@ var updateIssueInGuiState = (state, issueId, updateIssue) => ({
41552
42488
  }))
41553
42489
  });
41554
42490
 
42491
+ // source/gui/client/lib/socket-send.ts
42492
+ var pending = /* @__PURE__ */ new WeakMap();
42493
+ var sendSocketJson = (socket, data2) => {
42494
+ if (!socket) return;
42495
+ const message = JSON.stringify(data2);
42496
+ if (socket.readyState === WebSocket.OPEN) {
42497
+ socket.send(message);
42498
+ return;
42499
+ }
42500
+ if (socket.readyState !== WebSocket.CONNECTING) return;
42501
+ const queued = pending.get(socket);
42502
+ if (queued) {
42503
+ queued.push(message);
42504
+ return;
42505
+ }
42506
+ pending.set(socket, [message]);
42507
+ socket.addEventListener(
42508
+ "open",
42509
+ () => {
42510
+ for (const queuedMessage of pending.get(socket) ?? []) {
42511
+ socket.send(queuedMessage);
42512
+ }
42513
+ pending.delete(socket);
42514
+ },
42515
+ { once: true }
42516
+ );
42517
+ };
42518
+
41555
42519
  // source/gui/client/lib/history-buffer.ts
41556
42520
  var createHistoryBuffer = (publish) => {
41557
42521
  let lastRequestId = 0;
@@ -41571,7 +42535,48 @@ var createHistoryBuffer = (publish) => {
41571
42535
  const { timeline, commits } = pending2;
41572
42536
  if (!timeline || !commits) return;
41573
42537
  pending2 = {};
41574
- publish({ timeline, commits });
42538
+ publish({ requestId, timeline, commits });
42539
+ }
42540
+ };
42541
+ };
42542
+
42543
+ // source/gui/client/lib/gui-mutations.ts
42544
+ var MUTATING_MESSAGE_TYPES = /* @__PURE__ */ new Set([
42545
+ "sync",
42546
+ "issues:create",
42547
+ "issues:move",
42548
+ "issue:close",
42549
+ "issue:reopen",
42550
+ "issue:edit:title",
42551
+ "issue:edit:description",
42552
+ "issue:tag:add",
42553
+ "issue:tag:remove",
42554
+ "contributor:remove",
42555
+ "issue:assignee:add",
42556
+ "issue:assignee:remove",
42557
+ "issue:comment:add",
42558
+ "issue:comment:delete"
42559
+ ]);
42560
+
42561
+ // source/gui/client/lib/mutation-gate.ts
42562
+ var createMutationGate = () => {
42563
+ let pending2 = 0;
42564
+ return {
42565
+ sent(type) {
42566
+ if (MUTATING_MESSAGE_TYPES.has(type)) pending2 += 1;
42567
+ },
42568
+ /** `error` counts too: the handler paths that fail early send no result. */
42569
+ received(type) {
42570
+ if (type === "error" || type?.endsWith(":result")) {
42571
+ pending2 = Math.max(0, pending2 - 1);
42572
+ }
42573
+ },
42574
+ /** A reply owed on a closed socket never arrives. */
42575
+ reset() {
42576
+ pending2 = 0;
42577
+ },
42578
+ holdsState() {
42579
+ return pending2 > 0;
41575
42580
  }
41576
42581
  };
41577
42582
  };
@@ -41641,8 +42646,9 @@ var blobToBase64 = (blob) => new Promise((resolve, reject) => {
41641
42646
  });
41642
42647
 
41643
42648
  // source/gui/client/App.tsx
41644
- var import_jsx_runtime32 = __toESM(require_jsx_runtime(), 1);
41645
- var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42649
+ var import_jsx_runtime33 = __toESM(require_jsx_runtime(), 1);
42650
+ var EMPTY_COMMENTS = {};
42651
+ var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
41646
42652
  "div",
41647
42653
  {
41648
42654
  style: {
@@ -41655,38 +42661,44 @@ var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41655
42661
  }
41656
42662
  );
41657
42663
  var App = () => {
41658
- const { boardId, issueId } = useParams();
42664
+ const { boardId } = useParams();
42665
+ const issueMatch = useMatch("/board/:boardId/issue/:issueId");
42666
+ const legacyIssueMatch = useMatch("/board/:boardId/:issueId");
42667
+ const issueId = issueMatch?.params.issueId ?? legacyIssueMatch?.params.issueId;
41659
42668
  const [searchParams, setSearchParams] = useSearchParams();
41660
- const [connected, setConnected] = (0, import_react15.useState)(false);
41661
- const [syncStatus, setSyncStatus] = (0, import_react15.useState)({
42669
+ const [connected, setConnected] = (0, import_react16.useState)(false);
42670
+ const [syncStatus, setSyncStatus] = (0, import_react16.useState)({
41662
42671
  status: "synced",
41663
42672
  msg: "idle"
41664
42673
  });
41665
- const [state, setState] = (0, import_react15.useState)(null);
41666
- const [contributors, setContributors] = (0, import_react15.useState)([]);
41667
- const [history, setHistory] = (0, import_react15.useState)({ timeline: null, commits: [] });
41668
- const [historyBuffer] = (0, import_react15.useState)(() => createHistoryBuffer(setHistory));
41669
- const [commitInspectError, setCommitInspectError] = (0, import_react15.useState)(
42674
+ const [state, setState] = (0, import_react16.useState)(null);
42675
+ const [contributors, setContributors] = (0, import_react16.useState)([]);
42676
+ const [history, setHistory] = (0, import_react16.useState)({ requestId: 0, timeline: null, commits: [] });
42677
+ const [historyBuffer] = (0, import_react16.useState)(() => createHistoryBuffer(setHistory));
42678
+ const [commitInspectError, setCommitInspectError] = (0, import_react16.useState)(
41670
42679
  null
41671
42680
  );
41672
- const [removeError, setRemoveError] = (0, import_react15.useState)(null);
41673
- const [dragOverSwimlaneId, setDragOverSwimlaneId] = (0, import_react15.useState)(
42681
+ const [removeError, setRemoveError] = (0, import_react16.useState)(null);
42682
+ const [actionError, setActionError] = (0, import_react16.useState)(null);
42683
+ const [dragOverSwimlaneId, setDragOverSwimlaneId] = (0, import_react16.useState)(
41674
42684
  null
41675
42685
  );
41676
- const [dropTarget, setDropTarget] = (0, import_react15.useState)(null);
41677
- const [boardMenuOpen, setBoardMenuOpen] = (0, import_react15.useState)(false);
41678
- const [createIssueModal, setCreateIssueModal] = (0, import_react15.useState)(null);
41679
- const boardMenuRef = (0, import_react15.useRef)(null);
41680
- const socketRef = (0, import_react15.useRef)(null);
42686
+ const [dropTarget, setDropTarget] = (0, import_react16.useState)(null);
42687
+ const [pickedIssueIds, setPickedIssueIds] = (0, import_react16.useState)([]);
42688
+ const [boardMenuOpen, setBoardMenuOpen] = (0, import_react16.useState)(false);
42689
+ const [createIssueModal, setCreateIssueModal] = (0, import_react16.useState)(null);
42690
+ const boardMenuRef = (0, import_react16.useRef)(null);
42691
+ const socketRef = (0, import_react16.useRef)(null);
42692
+ const [mutationGate] = (0, import_react16.useState)(createMutationGate);
41681
42693
  const selectedTab = searchParams.get("tab") === "comments" ? "comments" : "overview";
41682
42694
  const navigate = useNavigate();
41683
- 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;
41684
42696
  const selectedBoardId = selectedBoard?.id ?? null;
41685
- const selectedBoardIdRef = (0, import_react15.useRef)(selectedBoardId);
42697
+ const selectedBoardIdRef = (0, import_react16.useRef)(selectedBoardId);
41686
42698
  selectedBoardIdRef.current = selectedBoardId;
41687
42699
  const boardSlug = selectedBoard?.ref ?? boardId;
41688
42700
  const selectedIssue = state && issueId ? findIssue(state, issueId) : null;
41689
- (0, import_react15.useEffect)(() => {
42701
+ (0, import_react16.useEffect)(() => {
41690
42702
  const onPaste = (event) => {
41691
42703
  if (!selectedIssue || selectedIssue.readonly) return;
41692
42704
  const target = event.target;
@@ -41705,33 +42717,69 @@ var App = () => {
41705
42717
  if (!boardSlug) return;
41706
42718
  void navigate(`/board/${boardSlug}`);
41707
42719
  };
41708
- 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]);
41709
42740
  const attachmentsByIssueId = state?.attachmentsByIssueId ?? {};
41710
- const [attachmentUploadStatus, setAttachmentUploadStatus] = (0, import_react15.useState)({ state: "idle" });
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]);
41711
42752
  const requestState = () => {
41712
42753
  sendSocketJson(socketRef.current, { type: "state:get" });
41713
42754
  };
41714
- (0, import_react15.useEffect)(() => {
42755
+ (0, import_react16.useEffect)(() => {
41715
42756
  const socket = new WebSocket(
41716
42757
  `ws://${window.location.host}/ws${boardId ? `?boardId=${boardId}` : ""}`
41717
42758
  );
41718
42759
  socketRef.current = socket;
41719
42760
  socket.addEventListener("open", () => {
41720
42761
  setConnected(true);
42762
+ mutationGate.reset();
41721
42763
  sendSocketJson(socket, { type: "state:get" });
41722
42764
  });
41723
42765
  socket.addEventListener("close", () => {
41724
42766
  setConnected(false);
42767
+ mutationGate.reset();
41725
42768
  if (socketRef.current === socket) {
41726
42769
  socketRef.current = null;
41727
42770
  }
41728
42771
  });
41729
42772
  socket.addEventListener("message", (event) => {
41730
42773
  const message = JSON.parse(event.data);
41731
- if (message.type === "state") {
42774
+ mutationGate.received(message.type);
42775
+ if (message.type === "state" && !mutationGate.holdsState()) {
41732
42776
  const nextState = getResultValue(message.payload);
41733
42777
  if (nextState) setState(nextState);
41734
42778
  }
42779
+ if (message.type === "issue") {
42780
+ const detail = getResultValue(message.payload);
42781
+ if (detail) setIssueDetail(detail);
42782
+ }
41735
42783
  if (message.type === "issue:created") {
41736
42784
  const created = getResultValue(message.payload);
41737
42785
  if (created && boardId) {
@@ -41741,7 +42789,9 @@ var App = () => {
41741
42789
  }
41742
42790
  }
41743
42791
  if (message.type === "failed") {
41744
- console.log("Failed", message);
42792
+ setActionError(
42793
+ typeof message.payload === "string" ? message.payload : "The board refused that change"
42794
+ );
41745
42795
  requestState();
41746
42796
  }
41747
42797
  if (message.type === "timeline") {
@@ -41789,12 +42839,13 @@ var App = () => {
41789
42839
  socket.close();
41790
42840
  };
41791
42841
  }, [boardId, navigate]);
41792
- (0, import_react15.useEffect)(() => {
41793
- if (!boardId && state?.boards[0]) {
41794
- void navigate(`/board/${state.boards[0].ref}`, { replace: true });
42842
+ (0, import_react16.useEffect)(() => {
42843
+ const first = state?.boards?.[0];
42844
+ if (!boardId && first) {
42845
+ void navigate(`/board/${first.ref}`, { replace: true });
41795
42846
  }
41796
42847
  }, [boardId, state, navigate]);
41797
- (0, import_react15.useEffect)(() => {
42848
+ (0, import_react16.useEffect)(() => {
41798
42849
  const close = (event) => {
41799
42850
  if (boardMenuRef.current && !boardMenuRef.current.contains(event.target)) {
41800
42851
  setBoardMenuOpen(false);
@@ -41808,9 +42859,24 @@ var App = () => {
41808
42859
  setDropTarget(null);
41809
42860
  };
41810
42861
  const send = (type, payload) => {
42862
+ mutationGate.sent(type);
41811
42863
  sendSocketJson(socketRef.current, { type, payload });
41812
42864
  };
41813
- const selectIssue = (nextIssueId) => {
42865
+ const togglePicked = (nextIssueId) => {
42866
+ setPickedIssueIds(
42867
+ (current) => current.includes(nextIssueId) ? current.filter((id) => id !== nextIssueId) : [...current, nextIssueId]
42868
+ );
42869
+ };
42870
+ const clearPicked = () => setPickedIssueIds([]);
42871
+ const [bulkTagName, setBulkTagName] = (0, import_react16.useState)("");
42872
+ const [bulkAssigneeName, setBulkAssigneeName] = (0, import_react16.useState)("");
42873
+ const pickedIssues = (state?.boards ?? []).flatMap((board) => board.swimlanes).flatMap((swimlane) => swimlane.issues).filter((issue) => pickedIssueIds.includes(issue.id));
42874
+ const forPicked = (act) => {
42875
+ for (const issue of pickedIssues) act(issue.id);
42876
+ };
42877
+ const selectIssue = (nextIssueId, { toggle } = { toggle: false }) => {
42878
+ if (toggle) return togglePicked(nextIssueId);
42879
+ setPickedIssueIds([nextIssueId]);
41814
42880
  if (!boardSlug) return;
41815
42881
  void navigate(
41816
42882
  `/board/${boardSlug}/issue/${nodeRef(nextIssueId)}?tab=overview`
@@ -41957,7 +43023,7 @@ var App = () => {
41957
43023
  const returnToLive = () => {
41958
43024
  send("time-travel:live", {});
41959
43025
  };
41960
- const requestBoardHistory = (0, import_react15.useCallback)(
43026
+ const requestBoardHistory = (0, import_react16.useCallback)(
41961
43027
  (start2, end, allBoards) => {
41962
43028
  const window2 = start2 !== void 0 ? { start: start2, end } : void 0;
41963
43029
  const requestId = historyBuffer.open();
@@ -41976,28 +43042,34 @@ var App = () => {
41976
43042
  },
41977
43043
  [selectedBoardId]
41978
43044
  );
41979
- const requestContributors = (0, import_react15.useCallback)(() => {
43045
+ const requestContributors = (0, import_react16.useCallback)(() => {
41980
43046
  sendSocketJson(socketRef.current, {
41981
43047
  type: "contributors:get",
41982
43048
  payload: { boardId: selectedBoardIdRef.current }
41983
43049
  });
41984
43050
  }, []);
41985
- (0, import_react15.useEffect)(() => {
43051
+ (0, import_react16.useEffect)(() => {
41986
43052
  setContributors([]);
43053
+ setPickedIssueIds([]);
41987
43054
  }, [selectedBoardId]);
41988
- const inspectCommit = (0, import_react15.useCallback)((sha) => {
43055
+ const inspectCommit = (0, import_react16.useCallback)((sha) => {
41989
43056
  sendSocketJson(socketRef.current, { type: "commit:inspect", payload: { sha } });
41990
43057
  }, []);
41991
- (0, import_react15.useEffect)(() => {
43058
+ (0, import_react16.useEffect)(() => {
41992
43059
  if (!commitInspectError) return;
41993
43060
  const timeout = setTimeout(() => setCommitInspectError(null), 8e3);
41994
43061
  return () => clearTimeout(timeout);
41995
43062
  }, [commitInspectError]);
41996
- (0, import_react15.useEffect)(() => {
43063
+ (0, import_react16.useEffect)(() => {
41997
43064
  if (!removeError) return;
41998
43065
  const timeout = setTimeout(() => setRemoveError(null), 8e3);
41999
43066
  return () => clearTimeout(timeout);
42000
43067
  }, [removeError]);
43068
+ (0, import_react16.useEffect)(() => {
43069
+ if (!actionError) return;
43070
+ const timeout = setTimeout(() => setActionError(null), 8e3);
43071
+ return () => clearTimeout(timeout);
43072
+ }, [actionError]);
42001
43073
  const selectBoard = (nextBoardId) => {
42002
43074
  setBoardMenuOpen(false);
42003
43075
  clearDragState();
@@ -42120,7 +43192,7 @@ var App = () => {
42120
43192
  });
42121
43193
  send("issue:comment:delete", { commentId });
42122
43194
  };
42123
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
43195
+ return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
42124
43196
  "div",
42125
43197
  {
42126
43198
  style: {
@@ -42132,37 +43204,46 @@ var App = () => {
42132
43204
  flexDirection: "column"
42133
43205
  },
42134
43206
  children: [
42135
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(GlobalScrollbarStyles, {}),
42136
- commitInspectError && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
43207
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(GlobalScrollbarStyles, {}),
43208
+ commitInspectError && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42137
43209
  ErrorToast,
42138
43210
  {
42139
43211
  message: `Couldn't open commit diff: ${commitInspectError}`,
42140
43212
  onDismiss: () => setCommitInspectError(null)
42141
43213
  }
42142
43214
  ),
42143
- removeError && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
43215
+ removeError && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42144
43216
  ErrorToast,
42145
43217
  {
42146
43218
  message: removeError,
42147
43219
  onDismiss: () => setRemoveError(null)
42148
43220
  }
42149
43221
  ),
42150
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Header, { state, connected, syncStatus }),
42151
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
43222
+ actionError && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43223
+ ErrorToast,
43224
+ {
43225
+ message: actionError,
43226
+ onDismiss: () => setActionError(null)
43227
+ }
43228
+ ),
43229
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Header, { state, connected, syncStatus }),
43230
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42152
43231
  TimeScrubber,
42153
43232
  {
42154
43233
  timeline: history.timeline,
42155
43234
  commits: history.commits,
43235
+ historyId: history.requestId,
42156
43236
  boardId: selectedBoardId,
42157
43237
  connected,
42158
43238
  onRequestHistory: requestBoardHistory,
42159
43239
  onInspectCommit: inspectCommit,
42160
43240
  timeTravel: state?.timeTravel ?? { mode: "live", asOfTime: null },
42161
43241
  onScrub: scrubToTime,
42162
- onReturnToLive: returnToLive
43242
+ onReturnToLive: returnToLive,
43243
+ onBoardFilterChange: setBoardFilter
42163
43244
  }
42164
43245
  ),
42165
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
43246
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
42166
43247
  "div",
42167
43248
  {
42168
43249
  style: {
@@ -42171,9 +43252,10 @@ var App = () => {
42171
43252
  overflow: "hidden"
42172
43253
  },
42173
43254
  children: [
42174
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
43255
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
42175
43256
  "main",
42176
43257
  {
43258
+ onClick: clearPicked,
42177
43259
  style: {
42178
43260
  // No bottom padding: the board row is the horizontal scroll
42179
43261
  // container, and a gap below it would strand its scrollbar.
@@ -42185,24 +43267,35 @@ var App = () => {
42185
43267
  overflow: "hidden"
42186
43268
  },
42187
43269
  children: [
42188
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { style: { padding: "20px 10px" }, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42189
- Dropdown,
43270
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43271
+ "div",
42190
43272
  {
42191
- testId: "board-switcher",
42192
- label: "Board:",
42193
- value: selectedBoard ? {
42194
- id: selectedBoard.id,
42195
- label: selectedBoard.title
42196
- } : null,
42197
- items: state?.boards.map((board) => ({
42198
- id: board.id,
42199
- label: board.title
42200
- })) ?? [],
42201
- placeholder: "Loading...",
42202
- 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
+ )
42203
43296
  }
42204
- ) }),
42205
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
43297
+ ),
43298
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
42206
43299
  "div",
42207
43300
  {
42208
43301
  style: {
@@ -42214,7 +43307,7 @@ var App = () => {
42214
43307
  overflowY: "hidden"
42215
43308
  },
42216
43309
  children: [
42217
- selectedBoard?.swimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
43310
+ visibleSwimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42218
43311
  SwimlaneColumn,
42219
43312
  {
42220
43313
  swimlane,
@@ -42226,27 +43319,64 @@ var App = () => {
42226
43319
  onSelectIssue: selectIssue,
42227
43320
  onSelectIssueComments: selectIssueComments,
42228
43321
  onCreateIssue: openCreateIssueModal,
42229
- onDropIssue: moveIssue(setState, socketRef),
43322
+ onDropIssue: (issueId2, swimlaneId, targetIndex) => {
43323
+ const moving = pickedIssueIds.includes(issueId2) ? pickedIssueIds : [issueId2];
43324
+ moveIssue(state, setState, send)(
43325
+ moving,
43326
+ swimlaneId,
43327
+ targetIndex
43328
+ );
43329
+ clearPicked();
43330
+ },
43331
+ pickedIssueIds,
42230
43332
  onDragOver: setDragOverSwimlaneId,
42231
43333
  onDragOverIssue: (swimlaneId, index2) => setDropTarget({ swimlaneId, index: index2 }),
42232
43334
  onDragLeave: clearDragState
42233
43335
  },
42234
43336
  swimlane.id
42235
43337
  )),
42236
- !(selectedIssue && state?.user) && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { style: { width: ASIDE_WIDTH, flexShrink: 0 } })
43338
+ !(selectedIssue && state?.user) && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("div", { style: { width: ASIDE_WIDTH, flexShrink: 0 } })
42237
43339
  ]
42238
43340
  }
42239
43341
  )
42240
43342
  ]
42241
43343
  }
42242
43344
  ),
42243
- selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
43345
+ pickedIssues.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
43346
+ BulkDetails,
43347
+ {
43348
+ issues: pickedIssues,
43349
+ knownTags: state?.tags ?? [],
43350
+ knownAssignees: contributors,
43351
+ tagName: bulkTagName,
43352
+ assigneeName: bulkAssigneeName,
43353
+ onChangeTagName: setBulkTagName,
43354
+ onChangeAssigneeName: setBulkAssigneeName,
43355
+ onAddTag: (name2) => {
43356
+ forPicked((id) => addIssueTag(id, name2));
43357
+ setBulkTagName("");
43358
+ },
43359
+ onRemoveTag: (tagId) => forPicked((id) => removeIssueTag(id, tagId)),
43360
+ onAddAssignee: (assigneeId) => forPicked((id) => addIssueAssignee(id, assigneeId)),
43361
+ onRemoveAssignee: (assigneeId) => forPicked((id) => removeIssueAssignee(id, assigneeId)),
43362
+ onCloseIssues: () => {
43363
+ forPicked(closeIssue);
43364
+ clearPicked();
43365
+ },
43366
+ onReopenIssues: () => {
43367
+ forPicked(reopenIssue);
43368
+ clearPicked();
43369
+ },
43370
+ onClear: clearPicked
43371
+ }
43372
+ ),
43373
+ pickedIssues.length <= 1 && selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42244
43374
  IssueDetails,
42245
43375
  {
42246
43376
  whoAmI: state.user,
42247
- issue: selectedIssue,
43377
+ issue: issueDetail?.issueId === selectedIssue.id ? { ...selectedIssue, description: issueDetail.description } : selectedIssue,
42248
43378
  activeTab: selectedTab,
42249
- comments: commentsByIssueId[selectedIssue.id] ?? [],
43379
+ comments: issueDetail?.issueId === selectedIssue.id ? issueDetail.comments : [],
42250
43380
  onChangeTab: changeIssueDetailsTab,
42251
43381
  onClose: closeIssueDetails,
42252
43382
  onEditTitle: editIssueTitle,
@@ -42273,7 +43403,7 @@ var App = () => {
42273
43403
  ]
42274
43404
  }
42275
43405
  ),
42276
- createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
43406
+ createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42277
43407
  CreateIssueModal,
42278
43408
  {
42279
43409
  title: createIssueModal.title,
@@ -42288,14 +43418,12 @@ var App = () => {
42288
43418
  };
42289
43419
 
42290
43420
  // source/gui/client/main.tsx
42291
- var import_jsx_runtime33 = __toESM(require_jsx_runtime(), 1);
43421
+ var import_jsx_runtime34 = __toESM(require_jsx_runtime(), 1);
42292
43422
  (0, import_client.createRoot)(document.getElementById("root")).render(
42293
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(BrowserRouter, { children: /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(Routes, { children: [
42294
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Route, { path: "/", element: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(App, {}) }),
42295
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Route, { path: "/board/:boardId", element: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(App, {}) }),
42296
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Route, { path: "/board/:boardId/issue/:issueId", element: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(App, {}) }),
42297
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Route, { path: "/board/:boardId/:issueId", element: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(App, {}) }),
42298
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Route, { path: "*", element: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(Navigate, { to: "/", replace: true }) })
43423
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(BrowserRouter, { children: /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(Routes, { children: [
43424
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Route, { path: "/", element: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(App, {}) }),
43425
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Route, { path: "/board/:boardId/*", element: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(App, {}) }),
43426
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Route, { path: "*", element: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(Navigate, { to: "/", replace: true }) })
42299
43427
  ] }) })
42300
43428
  );
42301
43429
  /*! Bundled license information: