epiq 1.3.5 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/gui/main.js CHANGED
@@ -4749,8 +4749,8 @@ var require_react_dom_client_development = __commonJS({
4749
4749
  var lane = concurrentQueues[i];
4750
4750
  concurrentQueues[i++] = null;
4751
4751
  if (null !== queue && null !== update) {
4752
- var pending = queue.pending;
4753
- null === pending ? update.next = update : (update.next = pending.next, pending.next = update);
4752
+ var pending2 = queue.pending;
4753
+ null === pending2 ? update.next = update : (update.next = pending2.next, pending2.next = update);
4754
4754
  queue.pending = update;
4755
4755
  }
4756
4756
  0 !== lane && markUpdateLaneFromFiberToRoot(fiber, update, lane);
@@ -8127,8 +8127,8 @@ var require_react_dom_client_development = __commonJS({
8127
8127
  }
8128
8128
  function enqueueRenderPhaseUpdate(queue, update) {
8129
8129
  didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
8130
- var pending = queue.pending;
8131
- null === pending ? update.next = update : (update.next = pending.next, pending.next = update);
8130
+ var pending2 = queue.pending;
8131
+ null === pending2 ? update.next = update : (update.next = pending2.next, pending2.next = update);
8132
8132
  queue.pending = update;
8133
8133
  }
8134
8134
  function entangleTransitionUpdate(root5, queue, lane) {
@@ -8245,14 +8245,14 @@ var require_react_dom_client_development = __commonJS({
8245
8245
  1,
8246
8246
  badgeFormat + " " + error[0],
8247
8247
  badgeStyle,
8248
- pad + JSCompiler_inline_result + pad,
8248
+ pad2 + JSCompiler_inline_result + pad2,
8249
8249
  resetStyle
8250
8250
  ) : error.splice(
8251
8251
  0,
8252
8252
  0,
8253
8253
  badgeFormat,
8254
8254
  badgeStyle,
8255
- pad + JSCompiler_inline_result + pad,
8255
+ pad2 + JSCompiler_inline_result + pad2,
8256
8256
  resetStyle
8257
8257
  );
8258
8258
  error.unshift(console);
@@ -21216,7 +21216,7 @@ var require_react_dom_client_development = __commonJS({
21216
21216
  _currentValue: NotPendingTransition,
21217
21217
  _currentValue2: NotPendingTransition,
21218
21218
  _threadCount: 0
21219
- }, badgeFormat = "%c%s%c", badgeStyle = "background: #e6e6e6;background: light-dark(rgba(0,0,0,0.1), rgba(255,255,255,0.25));color: #000000;color: light-dark(#000000, #ffffff);border-radius: 2px", resetStyle = "", pad = " ", bind = Function.prototype.bind;
21219
+ }, badgeFormat = "%c%s%c", badgeStyle = "background: #e6e6e6;background: light-dark(rgba(0,0,0,0.1), rgba(255,255,255,0.25));color: #000000;color: light-dark(#000000, #ffffff);border-radius: 2px", resetStyle = "", pad2 = " ", bind = Function.prototype.bind;
21220
21220
  var didWarnAboutNestedUpdates = false;
21221
21221
  var overrideHookState = null, overrideHookStateDeletePath = null, overrideHookStateRenamePath = null, overrideProps = null, overridePropsDeletePath = null, overridePropsRenamePath = null, scheduleUpdate = null, scheduleRetry = null, setErrorHandler = null, setSuspenseHandler = null;
21222
21222
  overrideHookState = function(fiber, id, path2, value) {
@@ -24825,7 +24825,7 @@ function useViewTransitionState(to, { relative } = {}) {
24825
24825
  }
24826
24826
 
24827
24827
  // source/gui/client/App.tsx
24828
- var import_react11 = __toESM(require_react(), 1);
24828
+ var import_react15 = __toESM(require_react(), 1);
24829
24829
 
24830
24830
  // source/gui/client/components/Aside.tsx
24831
24831
  var import_react = __toESM(require_react(), 1);
@@ -24841,20 +24841,12 @@ var GUI_THEME = {
24841
24841
  primary: "#c2c5d0",
24842
24842
  secondary: "#7f8aa3",
24843
24843
  tertiary: "rgb(31 33 43)",
24844
- dim: "#3d4151",
24845
- dim2: "rgb(71 77 92)",
24844
+ dim: "#585d78",
24845
+ dim2: "rgb(100 107 133)",
24846
24846
  accent: "#76d4ff",
24847
24847
  green: "#8ce99a",
24848
24848
  red: "#ff8787"
24849
24849
  };
24850
- var getContrastTextColor = (backgroundColor) => {
24851
- const hex = backgroundColor.replace("#", "");
24852
- const r = Number.parseInt(hex.slice(0, 2), 16);
24853
- const g = Number.parseInt(hex.slice(2, 4), 16);
24854
- const b = Number.parseInt(hex.slice(4, 6), 16);
24855
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
24856
- return luminance > 0.6 ? "#111111" : "#ffffff";
24857
- };
24858
24850
 
24859
24851
  // source/gui/client/components/Aside.tsx
24860
24852
  var import_jsx_runtime = __toESM(require_jsx_runtime(), 1);
@@ -25046,7 +25038,8 @@ var Dropdown = ({
25046
25038
  value,
25047
25039
  items,
25048
25040
  placeholder = "Select...",
25049
- onSelect
25041
+ onSelect,
25042
+ testId
25050
25043
  }) => {
25051
25044
  const [open, setOpen] = (0, import_react3.useState)(false);
25052
25045
  const ref = (0, import_react3.useRef)(null);
@@ -25076,6 +25069,7 @@ var Dropdown = ({
25076
25069
  Button,
25077
25070
  {
25078
25071
  variant: "ghost",
25072
+ "data-testid": testId,
25079
25073
  onClick: () => setOpen((value2) => !value2),
25080
25074
  style: {
25081
25075
  display: "flex",
@@ -25122,6 +25116,7 @@ var Dropdown = ({
25122
25116
  "button",
25123
25117
  {
25124
25118
  type: "button",
25119
+ "data-testid": testId ? `${testId}-option` : void 0,
25125
25120
  onClick: () => {
25126
25121
  setOpen(false);
25127
25122
  onSelect(item.id);
@@ -25159,6 +25154,7 @@ var Dropdown = ({
25159
25154
  // source/gui/client/components/Panel.tsx
25160
25155
  var import_react4 = __toESM(require_react(), 1);
25161
25156
  var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
25157
+ var POINTER_SAMPLE_MS = 16;
25162
25158
  var Panel = ({
25163
25159
  as,
25164
25160
  children,
@@ -25166,6 +25162,8 @@ var Panel = ({
25166
25162
  borderColor = GUI_THEME.line,
25167
25163
  glowColor = GUI_THEME.secondary,
25168
25164
  glowOpacity = 0.25,
25165
+ glowRadius = 200,
25166
+ proximityReach,
25169
25167
  borderRadius = 12,
25170
25168
  style,
25171
25169
  ...props
@@ -25173,6 +25171,38 @@ var Panel = ({
25173
25171
  const Component4 = as ?? "div";
25174
25172
  const [mouse, setMouse] = (0, import_react4.useState)({ x: 0, y: 0 });
25175
25173
  const [hovered, setHovered] = (0, import_react4.useState)(false);
25174
+ const [proximity, setProximity] = (0, import_react4.useState)(0);
25175
+ const glowRef = (0, import_react4.useRef)(null);
25176
+ const lastMeasuredRef = (0, import_react4.useRef)(0);
25177
+ (0, import_react4.useEffect)(() => {
25178
+ if (proximityReach === void 0) return;
25179
+ const onMove = (event) => {
25180
+ const now = Date.now();
25181
+ if (now - lastMeasuredRef.current < POINTER_SAMPLE_MS) return;
25182
+ lastMeasuredRef.current = now;
25183
+ const panel = glowRef.current?.parentElement;
25184
+ if (!panel) return;
25185
+ const rect = panel.getBoundingClientRect();
25186
+ const dx = Math.max(
25187
+ rect.left - event.clientX,
25188
+ 0,
25189
+ event.clientX - rect.right
25190
+ );
25191
+ const dy = Math.max(
25192
+ rect.top - event.clientY,
25193
+ 0,
25194
+ event.clientY - rect.bottom
25195
+ );
25196
+ const distance = Math.hypot(dx, dy);
25197
+ setProximity(
25198
+ distance >= proximityReach ? 0 : 1 - distance / proximityReach
25199
+ );
25200
+ setMouse({ x: event.clientX - rect.left, y: event.clientY - rect.top });
25201
+ };
25202
+ window.addEventListener("mousemove", onMove);
25203
+ return () => window.removeEventListener("mousemove", onMove);
25204
+ }, [proximityReach]);
25205
+ const glowStrength = active ? 1 : proximityReach !== void 0 ? proximity : hovered ? 1 : 0;
25176
25206
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
25177
25207
  Component4,
25178
25208
  {
@@ -25204,15 +25234,16 @@ var Panel = ({
25204
25234
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
25205
25235
  "div",
25206
25236
  {
25237
+ ref: glowRef,
25207
25238
  style: {
25208
25239
  position: "absolute",
25209
25240
  inset: 0,
25210
25241
  borderRadius,
25211
25242
  padding: 1,
25212
25243
  pointerEvents: "none",
25213
- opacity: hovered || active ? glowOpacity : 0,
25244
+ opacity: glowStrength * glowOpacity,
25214
25245
  background: `radial-gradient(
25215
- 200px circle at ${mouse.x}px ${mouse.y}px,
25246
+ ${glowRadius}px circle at ${mouse.x}px ${mouse.y}px,
25216
25247
  ${glowColor},
25217
25248
  transparent 100%
25218
25249
  )`,
@@ -25260,12 +25291,13 @@ var User = ({
25260
25291
  display: "flex",
25261
25292
  alignItems: "center",
25262
25293
  justifyContent: "center",
25263
- background: user.color,
25264
- color: getContrastTextColor(user.color),
25294
+ background: "transparent",
25295
+ color: user.color,
25296
+ border: `1px solid ${user.color}`,
25265
25297
  fontSize: 11,
25266
25298
  fontWeight: 700,
25267
- marginLeft: index2 === 0 ? 0 : -6,
25268
- border: `2px solid #1a1a1a`
25299
+ marginLeft: index2 === 0 ? 0 : 4,
25300
+ flexShrink: 0
25269
25301
  },
25270
25302
  children: user.name.at(0)?.toUpperCase()
25271
25303
  },
@@ -25273,7 +25305,7 @@ var User = ({
25273
25305
  );
25274
25306
 
25275
25307
  // source/version.ts
25276
- var EPIQ_VERSION = "1.3.5";
25308
+ var EPIQ_VERSION = "1.4.0";
25277
25309
 
25278
25310
  // source/gui/client/components/Header.tsx
25279
25311
  var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
@@ -25410,11 +25442,202 @@ var Header = ({ state, connected, syncStatus }) => {
25410
25442
  };
25411
25443
 
25412
25444
  // source/gui/client/components/IssueDetails.tsx
25413
- var import_react9 = __toESM(require_react(), 1);
25445
+ var import_react10 = __toESM(require_react(), 1);
25414
25446
 
25415
- // source/gui/client/components/CopyRef.tsx
25447
+ // source/gui/client/components/ManageContributorsModal.tsx
25416
25448
  var import_react5 = __toESM(require_react(), 1);
25449
+
25450
+ // source/gui/client/components/IconLock.tsx
25417
25451
  var import_jsx_runtime8 = __toESM(require_jsx_runtime(), 1);
25452
+ var IconLock = ({ size = 13 }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
25453
+ "svg",
25454
+ {
25455
+ width: size,
25456
+ height: size,
25457
+ viewBox: "0 0 24 24",
25458
+ fill: "none",
25459
+ stroke: "currentColor",
25460
+ strokeWidth: "2",
25461
+ strokeLinecap: "round",
25462
+ strokeLinejoin: "round",
25463
+ "aria-hidden": "true",
25464
+ children: [
25465
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }),
25466
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" }),
25467
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "12", cy: "16.5", r: "2.1", fill: "currentColor", stroke: "none" })
25468
+ ]
25469
+ }
25470
+ );
25471
+
25472
+ // source/gui/client/components/IconTrash.tsx
25473
+ var import_jsx_runtime9 = __toESM(require_jsx_runtime(), 1);
25474
+ var IconTrash = ({ size = 14 }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
25475
+ "svg",
25476
+ {
25477
+ width: size,
25478
+ height: size,
25479
+ viewBox: "0 0 24 24",
25480
+ fill: "none",
25481
+ stroke: "currentColor",
25482
+ strokeWidth: "2",
25483
+ strokeLinecap: "round",
25484
+ strokeLinejoin: "round",
25485
+ "aria-hidden": "true",
25486
+ children: [
25487
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("polyline", { points: "3 6 5 6 21 6" }),
25488
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }),
25489
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "10", y1: "11", x2: "10", y2: "17" }),
25490
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "14", y1: "11", x2: "14", y2: "17" })
25491
+ ]
25492
+ }
25493
+ );
25494
+
25495
+ // source/gui/client/components/ManageContributorsModal.tsx
25496
+ var import_jsx_runtime10 = __toESM(require_jsx_runtime(), 1);
25497
+ var ManageContributorsModal = ({
25498
+ contributors,
25499
+ onRemove,
25500
+ onClose
25501
+ }) => {
25502
+ const [armedId, setArmedId] = (0, import_react5.useState)(null);
25503
+ const sorted = [...contributors].sort((a, b) => a.name.localeCompare(b.name));
25504
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25505
+ "div",
25506
+ {
25507
+ style: {
25508
+ position: "fixed",
25509
+ inset: 0,
25510
+ background: "rgba(0, 0, 0, 0.25)",
25511
+ backdropFilter: "blur(.5px)",
25512
+ display: "flex",
25513
+ alignItems: "center",
25514
+ justifyContent: "center",
25515
+ zIndex: 1e3
25516
+ },
25517
+ onMouseDown: onClose,
25518
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25519
+ "div",
25520
+ {
25521
+ onMouseDown: (event) => event.stopPropagation(),
25522
+ style: {
25523
+ marginTop: "-100px",
25524
+ width: 460,
25525
+ background: GUI_THEME.panel,
25526
+ border: `1px solid ${GUI_THEME.line}`,
25527
+ borderRadius: 12,
25528
+ padding: 20,
25529
+ display: "flex",
25530
+ flexDirection: "column",
25531
+ gap: 14
25532
+ },
25533
+ children: [
25534
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25535
+ "div",
25536
+ {
25537
+ style: {
25538
+ color: GUI_THEME.secondary,
25539
+ fontSize: 10,
25540
+ letterSpacing: 1,
25541
+ textTransform: "uppercase"
25542
+ },
25543
+ children: "Manage external contributors"
25544
+ }
25545
+ ),
25546
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { fontSize: 12, color: GUI_THEME.secondary }, children: "Remove contributors from the suggestion lists. Locked ones have contributed, so their name is in the immutable log either way." }),
25547
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25548
+ "div",
25549
+ {
25550
+ style: {
25551
+ display: "flex",
25552
+ flexDirection: "column",
25553
+ gap: 2,
25554
+ maxHeight: 260,
25555
+ overflowY: "auto"
25556
+ },
25557
+ children: sorted.map((contributor) => {
25558
+ const locked = contributor.hasAuthoredAnywhere;
25559
+ const armed = armedId === contributor.id;
25560
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25561
+ "div",
25562
+ {
25563
+ style: {
25564
+ display: "flex",
25565
+ alignItems: "center",
25566
+ justifyContent: "space-between",
25567
+ gap: 8,
25568
+ padding: "6px 8px",
25569
+ borderRadius: 6
25570
+ },
25571
+ children: [
25572
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25573
+ "span",
25574
+ {
25575
+ style: {
25576
+ fontSize: 12,
25577
+ color: contributor.color,
25578
+ opacity: locked ? 0.5 : 1
25579
+ },
25580
+ children: [
25581
+ "@",
25582
+ contributor.name
25583
+ ]
25584
+ }
25585
+ ),
25586
+ locked ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25587
+ "span",
25588
+ {
25589
+ title: "Has contributed \u2014 their name is in the immutable log either way",
25590
+ style: {
25591
+ display: "flex",
25592
+ color: GUI_THEME.dim,
25593
+ opacity: 0.7,
25594
+ // Matches the ghost button's padding so locked and
25595
+ // removable rows keep the same height.
25596
+ padding: "3px 6px"
25597
+ },
25598
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(IconLock, {})
25599
+ }
25600
+ ) : contributor.isRemoved ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: GUI_THEME.dim, fontSize: 10 }, children: "removed" }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25601
+ Button,
25602
+ {
25603
+ variant: "ghost",
25604
+ title: armed ? `Click again to remove @${contributor.name}` : `Remove @${contributor.name}`,
25605
+ onClick: () => {
25606
+ if (!armed) return setArmedId(contributor.id);
25607
+ onRemove(contributor.id);
25608
+ setArmedId(null);
25609
+ },
25610
+ style: {
25611
+ display: "flex",
25612
+ alignItems: "center",
25613
+ gap: 6,
25614
+ color: armed ? GUI_THEME.red : GUI_THEME.dim,
25615
+ borderColor: armed ? GUI_THEME.red : void 0
25616
+ },
25617
+ children: [
25618
+ armed && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { fontSize: 10 }, children: "confirm" }),
25619
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(IconTrash, {})
25620
+ ]
25621
+ }
25622
+ )
25623
+ ]
25624
+ },
25625
+ contributor.id
25626
+ );
25627
+ })
25628
+ }
25629
+ ),
25630
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { display: "flex", justifyContent: "flex-end" }, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Button, { variant: "ghost", onClick: onClose, children: "close" }) })
25631
+ ]
25632
+ }
25633
+ )
25634
+ }
25635
+ );
25636
+ };
25637
+
25638
+ // source/gui/client/components/CopyRef.tsx
25639
+ var import_react6 = __toESM(require_react(), 1);
25640
+ var import_jsx_runtime11 = __toESM(require_jsx_runtime(), 1);
25418
25641
  var copyToClipboard = async (value) => {
25419
25642
  try {
25420
25643
  await navigator.clipboard.writeText(value);
@@ -25424,12 +25647,12 @@ var copyToClipboard = async (value) => {
25424
25647
  }
25425
25648
  };
25426
25649
  var CopyRef = ({ refValue }) => {
25427
- const [copied, setCopied] = (0, import_react5.useState)(false);
25428
- const resetTimer = (0, import_react5.useRef)(
25650
+ const [copied, setCopied] = (0, import_react6.useState)(false);
25651
+ const resetTimer = (0, import_react6.useRef)(
25429
25652
  void 0
25430
25653
  );
25431
- (0, import_react5.useEffect)(() => () => clearTimeout(resetTimer.current), []);
25432
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
25654
+ (0, import_react6.useEffect)(() => () => clearTimeout(resetTimer.current), []);
25655
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
25433
25656
  "button",
25434
25657
  {
25435
25658
  type: "button",
@@ -25464,8 +25687,8 @@ var CopyRef = ({ refValue }) => {
25464
25687
  };
25465
25688
 
25466
25689
  // source/gui/client/components/FormHeader.tsx
25467
- var import_jsx_runtime9 = __toESM(require_jsx_runtime(), 1);
25468
- var FormHeader = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
25690
+ var import_jsx_runtime12 = __toESM(require_jsx_runtime(), 1);
25691
+ var FormHeader = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25469
25692
  "div",
25470
25693
  {
25471
25694
  style: {
@@ -25480,8 +25703,8 @@ var FormHeader = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
25480
25703
  );
25481
25704
 
25482
25705
  // source/gui/client/components/FormPrimitives.tsx
25483
- var import_jsx_runtime10 = __toESM(require_jsx_runtime(), 1);
25484
- var Input = (props) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25706
+ var import_jsx_runtime13 = __toESM(require_jsx_runtime(), 1);
25707
+ var Input = (props) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25485
25708
  "input",
25486
25709
  {
25487
25710
  ...props,
@@ -25499,7 +25722,7 @@ var Input = (props) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25499
25722
  }
25500
25723
  }
25501
25724
  );
25502
- var Textarea = (props) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25725
+ var Textarea = (props) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25503
25726
  "textarea",
25504
25727
  {
25505
25728
  maxLength: props.maxLength ?? 1500,
@@ -25521,28 +25744,28 @@ var Textarea = (props) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25521
25744
  }
25522
25745
  }
25523
25746
  );
25524
- var ActionRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25747
+ var ActionRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25525
25748
  "div",
25526
25749
  {
25527
25750
  style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 8 },
25528
25751
  children
25529
25752
  }
25530
25753
  );
25531
- var AddRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { display: "flex", gap: 8, marginTop: 10 }, children });
25532
- var ChipRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10 }, children });
25533
- var Empty = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { display: "inline-block", marginTop: 8, color: GUI_THEME.dim }, children });
25754
+ var AddRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { style: { display: "flex", gap: 8, marginTop: 10 }, children });
25755
+ var ChipRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10 }, children });
25756
+ var Empty = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { style: { display: "inline-block", marginTop: 8, color: GUI_THEME.dim }, children });
25534
25757
 
25535
25758
  // source/gui/client/components/IssueAttachments.tsx
25536
- var import_react6 = __toESM(require_react(), 1);
25759
+ var import_react7 = __toESM(require_react(), 1);
25537
25760
 
25538
25761
  // source/gui/client/components/Section.tsx
25539
- var import_jsx_runtime11 = __toESM(require_jsx_runtime(), 1);
25762
+ var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
25540
25763
  var Section = ({
25541
25764
  title,
25542
25765
  action,
25543
25766
  children,
25544
25767
  first = false
25545
- }) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
25768
+ }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25546
25769
  "section",
25547
25770
  {
25548
25771
  style: {
@@ -25550,7 +25773,7 @@ var Section = ({
25550
25773
  borderTop: first ? "none" : `1px solid ${GUI_THEME.line}`
25551
25774
  },
25552
25775
  children: [
25553
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
25776
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25554
25777
  "div",
25555
25778
  {
25556
25779
  style: {
@@ -25560,7 +25783,7 @@ var Section = ({
25560
25783
  gap: 12
25561
25784
  },
25562
25785
  children: [
25563
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
25786
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25564
25787
  "span",
25565
25788
  {
25566
25789
  style: {
@@ -25582,7 +25805,7 @@ var Section = ({
25582
25805
  );
25583
25806
 
25584
25807
  // source/gui/client/components/IssueAttachments.tsx
25585
- var import_jsx_runtime12 = __toESM(require_jsx_runtime(), 1);
25808
+ var import_jsx_runtime15 = __toESM(require_jsx_runtime(), 1);
25586
25809
  var IssueAttachments = ({
25587
25810
  issueId,
25588
25811
  readonly,
@@ -25591,15 +25814,15 @@ var IssueAttachments = ({
25591
25814
  onUploadFiles,
25592
25815
  onDeleteAttachment
25593
25816
  }) => {
25594
- const [dragging, setDragging] = (0, import_react6.useState)(false);
25595
- const [lightbox, setLightbox] = (0, import_react6.useState)(null);
25596
- const [broken, setBroken] = (0, import_react6.useState)({});
25597
- (0, import_react6.useEffect)(() => {
25817
+ const [dragging, setDragging] = (0, import_react7.useState)(false);
25818
+ const [lightbox, setLightbox] = (0, import_react7.useState)(null);
25819
+ const [broken, setBroken] = (0, import_react7.useState)({});
25820
+ (0, import_react7.useEffect)(() => {
25598
25821
  setLightbox(null);
25599
25822
  setDragging(false);
25600
25823
  setBroken({});
25601
25824
  }, [issueId]);
25602
- (0, import_react6.useEffect)(() => {
25825
+ (0, import_react7.useEffect)(() => {
25603
25826
  if (!lightbox) return;
25604
25827
  const onKeyDown = (event) => {
25605
25828
  if (event.key === "Escape") setLightbox(null);
@@ -25617,12 +25840,12 @@ var IssueAttachments = ({
25617
25840
  );
25618
25841
  if (files.length > 0) onUploadFiles?.(issueId, files);
25619
25842
  };
25620
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25843
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25621
25844
  Section,
25622
25845
  {
25623
25846
  title: `Attachments${attachments.length ? ` (${attachments.length})` : ""}`,
25624
25847
  children: [
25625
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25848
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25626
25849
  "div",
25627
25850
  {
25628
25851
  onDragOver: (event) => {
@@ -25640,7 +25863,7 @@ var IssueAttachments = ({
25640
25863
  transition: "padding 0.12s ease, border-color 0.12s ease"
25641
25864
  },
25642
25865
  children: [
25643
- attachments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Empty, { children: canUpload ? "No attachments \u2014 drop an image here" : "No attachments" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25866
+ attachments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Empty, { children: canUpload ? "No attachments \u2014 drop an image here" : "No attachments" }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25644
25867
  "div",
25645
25868
  {
25646
25869
  style: {
@@ -25648,12 +25871,12 @@ var IssueAttachments = ({
25648
25871
  flexWrap: "wrap",
25649
25872
  gap: 8
25650
25873
  },
25651
- children: attachments.map((attachment) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25874
+ children: attachments.map((attachment) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25652
25875
  "div",
25653
25876
  {
25654
25877
  style: { position: "relative", width: 72, height: 72 },
25655
25878
  children: [
25656
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25879
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25657
25880
  "button",
25658
25881
  {
25659
25882
  type: "button",
@@ -25673,7 +25896,7 @@ var IssueAttachments = ({
25673
25896
  width: "100%",
25674
25897
  height: "100%"
25675
25898
  },
25676
- children: broken[attachment.id] ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25899
+ children: broken[attachment.id] ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25677
25900
  "span",
25678
25901
  {
25679
25902
  title: "Attachment unavailable (missing, corrupt, or over the size cap)",
@@ -25690,7 +25913,7 @@ var IssueAttachments = ({
25690
25913
  },
25691
25914
  children: "unavailable"
25692
25915
  }
25693
- ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25916
+ ) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25694
25917
  "img",
25695
25918
  {
25696
25919
  src: `/media/${attachment.fileName}`,
@@ -25707,7 +25930,7 @@ var IssueAttachments = ({
25707
25930
  )
25708
25931
  }
25709
25932
  ),
25710
- attachment.canDelete && !readonly && onDeleteAttachment && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25933
+ attachment.canDelete && !readonly && onDeleteAttachment && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25711
25934
  "button",
25712
25935
  {
25713
25936
  type: "button",
@@ -25737,7 +25960,7 @@ var IssueAttachments = ({
25737
25960
  ))
25738
25961
  }
25739
25962
  ),
25740
- canUpload && attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25963
+ canUpload && attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25741
25964
  "div",
25742
25965
  {
25743
25966
  style: {
@@ -25748,7 +25971,7 @@ var IssueAttachments = ({
25748
25971
  children: "drop an image to attach"
25749
25972
  }
25750
25973
  ),
25751
- uploadStatus.state === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25974
+ uploadStatus.state === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25752
25975
  "div",
25753
25976
  {
25754
25977
  style: {
@@ -25763,7 +25986,7 @@ var IssueAttachments = ({
25763
25986
  ]
25764
25987
  }
25765
25988
  ),
25766
- uploadStatus.state === "error" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25989
+ uploadStatus.state === "error" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25767
25990
  "div",
25768
25991
  {
25769
25992
  style: {
@@ -25777,7 +26000,7 @@ var IssueAttachments = ({
25777
26000
  ]
25778
26001
  }
25779
26002
  ),
25780
- lightbox && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
26003
+ lightbox && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25781
26004
  "div",
25782
26005
  {
25783
26006
  onClick: () => setLightbox(null),
@@ -25794,7 +26017,7 @@ var IssueAttachments = ({
25794
26017
  cursor: "zoom-out"
25795
26018
  },
25796
26019
  children: [
25797
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
26020
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25798
26021
  "img",
25799
26022
  {
25800
26023
  src: `/media/${lightbox.fileName}`,
@@ -25807,7 +26030,7 @@ var IssueAttachments = ({
25807
26030
  }
25808
26031
  }
25809
26032
  ),
25810
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 11 }, children: [
26033
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 11 }, children: [
25811
26034
  lightbox.name,
25812
26035
  " \xB7 ",
25813
26036
  Math.max(1, Math.round(lightbox.bytes / 1024)),
@@ -25823,7 +26046,7 @@ var IssueAttachments = ({
25823
26046
  };
25824
26047
 
25825
26048
  // source/gui/client/components/IssueComments.tsx
25826
- var import_react8 = __toESM(require_react(), 1);
26049
+ var import_react9 = __toESM(require_react(), 1);
25827
26050
 
25828
26051
  // source/gui/client/lib/gui-format.helper.ts
25829
26052
  var timeAgo = (timestampMs) => {
@@ -27479,11 +27702,11 @@ function addChildren(props, children) {
27479
27702
  }
27480
27703
  }
27481
27704
  }
27482
- function productionCreate(_, jsx23, jsxs15) {
27705
+ function productionCreate(_, jsx34, jsxs22) {
27483
27706
  return create2;
27484
27707
  function create2(_2, type, props, key) {
27485
27708
  const isStaticChildren = Array.isArray(props.children);
27486
- const fn = isStaticChildren ? jsxs15 : jsx23;
27709
+ const fn = isStaticChildren ? jsxs22 : jsx34;
27487
27710
  return key ? fn(type, props, key) : fn(type, props);
27488
27711
  }
27489
27712
  }
@@ -27728,8 +27951,8 @@ var urlAttributes = {
27728
27951
  };
27729
27952
 
27730
27953
  // node_modules/react-markdown/lib/index.js
27731
- var import_jsx_runtime13 = __toESM(require_jsx_runtime(), 1);
27732
- var import_react7 = __toESM(require_react(), 1);
27954
+ var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
27955
+ var import_react8 = __toESM(require_react(), 1);
27733
27956
 
27734
27957
  // node_modules/mdast-util-to-string/lib/index.js
27735
27958
  var emptyOptions2 = {};
@@ -35403,11 +35626,11 @@ function post(tree, options) {
35403
35626
  }
35404
35627
  visit(tree, transform);
35405
35628
  return toJsxRuntime(tree, {
35406
- Fragment: import_jsx_runtime13.Fragment,
35629
+ Fragment: import_jsx_runtime16.Fragment,
35407
35630
  components: components2,
35408
35631
  ignoreInvalidStyle: true,
35409
- jsx: import_jsx_runtime13.jsx,
35410
- jsxs: import_jsx_runtime13.jsxs,
35632
+ jsx: import_jsx_runtime16.jsx,
35633
+ jsxs: import_jsx_runtime16.jsxs,
35411
35634
  passKeys: true,
35412
35635
  passNode: true
35413
35636
  });
@@ -38530,16 +38753,16 @@ function remarkGfm(options) {
38530
38753
  }
38531
38754
 
38532
38755
  // source/gui/client/components/MarkdownContent.tsx
38533
- var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
38756
+ var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
38534
38757
  var components = {
38535
- p: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { style: { margin: "0 0 14px" }, children }),
38536
- ul: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("ul", { style: { margin: "0 0 14px", paddingLeft: 20 }, children }),
38537
- ol: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("ol", { style: { margin: "0 0 14px", paddingLeft: 20 }, children }),
38538
- li: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("li", { style: { margin: "2px 0" }, children }),
38539
- h1: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h1", { style: { fontSize: 16, margin: "0 0 14px" }, children }),
38540
- h2: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h2", { style: { fontSize: 14, margin: "0 0 14px" }, children }),
38541
- h3: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("h3", { style: { fontSize: 13, margin: "0 0 14px" }, children }),
38542
- a: ({ children, href }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
38758
+ p: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { style: { margin: "0 0 14px" }, children }),
38759
+ ul: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ul", { style: { margin: "0 0 14px", paddingLeft: 20 }, children }),
38760
+ ol: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ol", { style: { margin: "0 0 14px", paddingLeft: 20 }, children }),
38761
+ li: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("li", { style: { margin: "2px 0" }, children }),
38762
+ h1: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h1", { style: { fontSize: 16, margin: "0 0 14px" }, children }),
38763
+ h2: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h2", { style: { fontSize: 14, margin: "0 0 14px" }, children }),
38764
+ h3: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { style: { fontSize: 13, margin: "0 0 14px" }, children }),
38765
+ a: ({ children, href }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38543
38766
  "a",
38544
38767
  {
38545
38768
  href,
@@ -38549,7 +38772,7 @@ var components = {
38549
38772
  children
38550
38773
  }
38551
38774
  ),
38552
- blockquote: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
38775
+ blockquote: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38553
38776
  "blockquote",
38554
38777
  {
38555
38778
  style: {
@@ -38563,7 +38786,7 @@ var components = {
38563
38786
  ),
38564
38787
  code: ({ children, className }) => {
38565
38788
  const isBlock = Boolean(className);
38566
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
38789
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38567
38790
  "code",
38568
38791
  {
38569
38792
  style: {
@@ -38580,18 +38803,22 @@ var components = {
38580
38803
  }
38581
38804
  );
38582
38805
  },
38583
- pre: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("pre", { style: { margin: "0 0 14px" }, children }),
38584
- hr: () => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
38806
+ pre: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("pre", { style: { margin: "0 0 14px" }, children }),
38807
+ hr: () => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38585
38808
  "hr",
38586
38809
  {
38587
- style: { border: "none", borderTop: `1px solid ${GUI_THEME.line}`, margin: "12px 0" }
38810
+ style: {
38811
+ border: "none",
38812
+ borderTop: `1px solid ${GUI_THEME.line}`,
38813
+ margin: "12px 0"
38814
+ }
38588
38815
  }
38589
38816
  )
38590
38817
  };
38591
38818
  var MarkdownContent = ({
38592
38819
  content: content3,
38593
38820
  softBreaks = false
38594
- }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
38821
+ }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38595
38822
  "div",
38596
38823
  {
38597
38824
  style: {
@@ -38604,7 +38831,7 @@ var MarkdownContent = ({
38604
38831
  // collapsing, so the container doesn't end in dead whitespace.
38605
38832
  marginBottom: -14
38606
38833
  },
38607
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
38834
+ children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38608
38835
  Markdown,
38609
38836
  {
38610
38837
  remarkPlugins: softBreaks ? [remarkGfm, remarkBreaks] : [remarkGfm],
@@ -38616,7 +38843,7 @@ var MarkdownContent = ({
38616
38843
  );
38617
38844
 
38618
38845
  // source/gui/client/components/IssueComments.tsx
38619
- var import_jsx_runtime15 = __toESM(require_jsx_runtime(), 1);
38846
+ var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
38620
38847
  var IssueComments = ({
38621
38848
  whoAmI,
38622
38849
  issueId,
@@ -38625,15 +38852,15 @@ var IssueComments = ({
38625
38852
  onAddComment,
38626
38853
  onDeleteComment
38627
38854
  }) => {
38628
- const [body, setBody] = (0, import_react8.useState)("");
38855
+ const [body, setBody] = (0, import_react9.useState)("");
38629
38856
  const addComment = () => {
38630
38857
  const nextBody = body.trim();
38631
38858
  if (!nextBody) return;
38632
38859
  onAddComment?.(issueId, nextBody);
38633
38860
  setBody("");
38634
38861
  };
38635
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { children: [
38636
- comments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Empty, { children: "No comments" }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
38862
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
38863
+ comments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Empty, { children: "No comments" }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
38637
38864
  "div",
38638
38865
  {
38639
38866
  style: {
@@ -38643,7 +38870,7 @@ var IssueComments = ({
38643
38870
  background: GUI_THEME.tertiary
38644
38871
  },
38645
38872
  children: [
38646
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
38873
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
38647
38874
  "div",
38648
38875
  {
38649
38876
  style: {
@@ -38653,15 +38880,15 @@ var IssueComments = ({
38653
38880
  marginBottom: 8
38654
38881
  },
38655
38882
  children: [
38656
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 11 }, children: [
38883
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 11 }, children: [
38657
38884
  comment.author.name ?? "unknown",
38658
- comment.createdAt && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { style: { color: GUI_THEME.dim2 }, children: [
38885
+ comment.createdAt && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { style: { color: GUI_THEME.dim2 }, children: [
38659
38886
  " ",
38660
38887
  "\xB7 ",
38661
38888
  timeAgo(comment.createdAt)
38662
38889
  ] })
38663
38890
  ] }),
38664
- comment.author.id === whoAmI.id && onDeleteComment && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
38891
+ !readonly && comment.author.id === whoAmI.id && onDeleteComment && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
38665
38892
  Button,
38666
38893
  {
38667
38894
  variant: "ghost",
@@ -38676,13 +38903,13 @@ var IssueComments = ({
38676
38903
  ]
38677
38904
  }
38678
38905
  ),
38679
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(MarkdownContent, { content: comment.body, softBreaks: true })
38906
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MarkdownContent, { content: comment.body, softBreaks: true })
38680
38907
  ]
38681
38908
  },
38682
38909
  comment.id
38683
38910
  )) }),
38684
- !readonly && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { marginTop: 20 }, children: [
38685
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
38911
+ !readonly && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginTop: 20 }, children: [
38912
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
38686
38913
  Textarea,
38687
38914
  {
38688
38915
  maxLength: 120,
@@ -38702,18 +38929,18 @@ var IssueComments = ({
38702
38929
  }
38703
38930
  }
38704
38931
  ),
38705
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ActionRow, { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: addComment, children: "comment" }) })
38932
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ActionRow, { children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Button, { onClick: addComment, children: "comment" }) })
38706
38933
  ] })
38707
38934
  ] });
38708
38935
  };
38709
38936
 
38710
38937
  // source/gui/client/components/Tabs.tsx
38711
- var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
38938
+ var import_jsx_runtime19 = __toESM(require_jsx_runtime(), 1);
38712
38939
  var Tabs = ({
38713
38940
  activeTab,
38714
38941
  tabs,
38715
38942
  onChange
38716
- }) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
38943
+ }) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
38717
38944
  "div",
38718
38945
  {
38719
38946
  style: {
@@ -38724,7 +38951,7 @@ var Tabs = ({
38724
38951
  },
38725
38952
  children: tabs.map((tab2) => {
38726
38953
  const active = tab2.id === activeTab;
38727
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
38954
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
38728
38955
  "button",
38729
38956
  {
38730
38957
  type: "button",
@@ -38742,7 +38969,7 @@ var Tabs = ({
38742
38969
  },
38743
38970
  children: [
38744
38971
  tab2.label,
38745
- typeof tab2.count === "number" && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
38972
+ typeof tab2.count === "number" && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
38746
38973
  "span",
38747
38974
  {
38748
38975
  style: { color: tab2.count ? GUI_THEME.secondary : GUI_THEME.dim },
@@ -38763,7 +38990,7 @@ var Tabs = ({
38763
38990
  );
38764
38991
 
38765
38992
  // source/gui/client/components/IssueDetails.tsx
38766
- var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
38993
+ var import_jsx_runtime20 = __toESM(require_jsx_runtime(), 1);
38767
38994
  var IssueDetails = ({
38768
38995
  whoAmI,
38769
38996
  comments,
@@ -38776,6 +39003,8 @@ var IssueDetails = ({
38776
39003
  onAddTag,
38777
39004
  onRemoveTag,
38778
39005
  onAddAssignee,
39006
+ onAddExternalAssignee,
39007
+ onRemoveContributor,
38779
39008
  onRemoveAssignee,
38780
39009
  onCloseIssue,
38781
39010
  onReopenIssue,
@@ -38786,28 +39015,30 @@ var IssueDetails = ({
38786
39015
  onUploadAttachments,
38787
39016
  onDeleteAttachment,
38788
39017
  knownTags: tags,
38789
- knownAssignees: assignees
39018
+ knownAssignees: assignees,
39019
+ onOpenAssigneePicker
38790
39020
  }) => {
38791
- const [title, setTitle] = (0, import_react9.useState)("");
38792
- const [description, setDescription] = (0, import_react9.useState)("");
38793
- const [tagName, setTagName] = (0, import_react9.useState)("");
38794
- const [assigneeName, setAssigneeName] = (0, import_react9.useState)("");
38795
- const [editingTitle, setEditingTitle] = (0, import_react9.useState)(false);
38796
- const [editingDescription, setEditingDescription] = (0, import_react9.useState)(false);
38797
- const [addingTag, setAddingTag] = (0, import_react9.useState)(false);
38798
- const [addingAssignee, setAddingAssignee] = (0, import_react9.useState)(false);
38799
- const panelRef = (0, import_react9.useRef)(null);
38800
- const titleTextareaRef = (0, import_react9.useRef)(null);
39021
+ const [title, setTitle] = (0, import_react10.useState)("");
39022
+ const [description, setDescription] = (0, import_react10.useState)("");
39023
+ const [tagName, setTagName] = (0, import_react10.useState)("");
39024
+ const [assigneeName, setAssigneeName] = (0, import_react10.useState)("");
39025
+ const [managingContributors, setManagingContributors] = (0, import_react10.useState)(false);
39026
+ const [editingTitle, setEditingTitle] = (0, import_react10.useState)(false);
39027
+ const [editingDescription, setEditingDescription] = (0, import_react10.useState)(false);
39028
+ const [addingTag, setAddingTag] = (0, import_react10.useState)(false);
39029
+ const [addingAssignee, setAddingAssignee] = (0, import_react10.useState)(false);
39030
+ const panelRef = (0, import_react10.useRef)(null);
39031
+ const titleTextareaRef = (0, import_react10.useRef)(null);
38801
39032
  const resizeTitleTextarea = () => {
38802
39033
  const el = titleTextareaRef.current;
38803
39034
  if (!el) return;
38804
39035
  el.style.height = "auto";
38805
39036
  el.style.height = `${el.scrollHeight}px`;
38806
39037
  };
38807
- (0, import_react9.useEffect)(() => {
39038
+ (0, import_react10.useEffect)(() => {
38808
39039
  if (editingTitle) resizeTitleTextarea();
38809
39040
  }, [editingTitle]);
38810
- (0, import_react9.useEffect)(() => {
39041
+ (0, import_react10.useEffect)(() => {
38811
39042
  if (!issue) return;
38812
39043
  const handleClickOutside = (event) => {
38813
39044
  if (panelRef.current && !panelRef.current.contains(event.target)) {
@@ -38817,7 +39048,7 @@ var IssueDetails = ({
38817
39048
  document.addEventListener("mousedown", handleClickOutside);
38818
39049
  return () => document.removeEventListener("mousedown", handleClickOutside);
38819
39050
  }, [issue, onClose]);
38820
- (0, import_react9.useEffect)(() => {
39051
+ (0, import_react10.useEffect)(() => {
38821
39052
  setTitle(issue?.title ?? "");
38822
39053
  setDescription(issue?.description ?? "");
38823
39054
  setTagName("");
@@ -38867,317 +39098,360 @@ var IssueDetails = ({
38867
39098
  };
38868
39099
  const addAssignee = () => {
38869
39100
  if (disabled || !issue || !assigneeName.trim()) return;
38870
- onAddAssignee(issue.id, assigneeName.trim());
39101
+ onAddExternalAssignee(issue.id, assigneeName.trim());
38871
39102
  setAssigneeName("");
38872
39103
  setAddingAssignee(false);
38873
39104
  };
38874
39105
  const availableTags = tags.filter(
38875
39106
  (tag) => !issue?.tags.some((issueTag) => issueTag.id === tag.id)
38876
39107
  );
39108
+ const assigneeRank = (assignee) => assignee.isSelf ? 0 : assignee.hasAuthoredAnywhere ? 1 : 2;
38877
39109
  const availableAssignees = assignees.filter(
38878
- (assignee) => !issue?.assignees.some((issueAssignee) => issueAssignee.id === assignee.id)
39110
+ (assignee) => !issue?.assignees.some(
39111
+ (issueAssignee) => issueAssignee.id === assignee.id
39112
+ )
39113
+ ).filter((assignee) => !assignee.isRemoved).sort(
39114
+ (a, b) => assigneeRank(a) - assigneeRank(b) || a.name.localeCompare(b.name)
38879
39115
  );
38880
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Aside, { ref: panelRef, children: issue ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
38881
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(FormHeader, { children: [
38882
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38883
- "span",
39116
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(Aside, { ref: panelRef, children: [
39117
+ issue ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
39118
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(FormHeader, { children: [
39119
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39120
+ "span",
39121
+ {
39122
+ style: {
39123
+ color: GUI_THEME.secondary,
39124
+ fontSize: 10,
39125
+ textTransform: "uppercase",
39126
+ letterSpacing: "0.08em"
39127
+ },
39128
+ children: issue.ref && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CopyRef, { refValue: issue.ref })
39129
+ }
39130
+ ),
39131
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Button, { variant: "ghost", onClick: onClose, children: "\xD7" })
39132
+ ] }),
39133
+ editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39134
+ "textarea",
38884
39135
  {
38885
- style: {
38886
- color: GUI_THEME.secondary,
38887
- fontSize: 10,
38888
- textTransform: "uppercase",
38889
- letterSpacing: "0.08em"
39136
+ ref: titleTextareaRef,
39137
+ value: title,
39138
+ autoFocus: true,
39139
+ rows: 1,
39140
+ onChange: (event) => {
39141
+ setTitle(event.target.value);
39142
+ resizeTitleTextarea();
38890
39143
  },
38891
- children: issue.ref && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CopyRef, { refValue: issue.ref })
38892
- }
38893
- ),
38894
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { variant: "ghost", onClick: onClose, children: "\xD7" })
38895
- ] }),
38896
- editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38897
- "textarea",
38898
- {
38899
- ref: titleTextareaRef,
38900
- value: title,
38901
- autoFocus: true,
38902
- rows: 1,
38903
- onChange: (event) => {
38904
- setTitle(event.target.value);
38905
- resizeTitleTextarea();
38906
- },
38907
- onKeyDown: (event) => {
38908
- if (event.key === "Enter") {
38909
- event.preventDefault();
38910
- event.currentTarget.blur();
39144
+ onKeyDown: (event) => {
39145
+ if (event.key === "Enter") {
39146
+ event.preventDefault();
39147
+ event.currentTarget.blur();
39148
+ }
39149
+ if (event.key === "Escape") cancelTitle();
39150
+ },
39151
+ onBlur: saveTitle,
39152
+ style: {
39153
+ display: "block",
39154
+ width: "100%",
39155
+ boxSizing: "border-box",
39156
+ resize: "none",
39157
+ overflow: "hidden",
39158
+ background: GUI_THEME.bg,
39159
+ color: GUI_THEME.primary,
39160
+ border: `1px solid ${GUI_THEME.line}`,
39161
+ borderRadius: 8,
39162
+ padding: "6px 10px",
39163
+ outline: "none",
39164
+ font: "inherit",
39165
+ fontSize: 18,
39166
+ fontWeight: 600,
39167
+ lineHeight: 1.35,
39168
+ marginBottom: 20
38911
39169
  }
38912
- if (event.key === "Escape") cancelTitle();
38913
- },
38914
- onBlur: saveTitle,
38915
- style: {
38916
- display: "block",
38917
- width: "100%",
38918
- boxSizing: "border-box",
38919
- resize: "none",
38920
- overflow: "hidden",
38921
- background: GUI_THEME.bg,
38922
- color: GUI_THEME.primary,
38923
- border: `1px solid ${GUI_THEME.line}`,
38924
- borderRadius: 8,
38925
- padding: "6px 10px",
38926
- outline: "none",
38927
- font: "inherit",
38928
- fontSize: 18,
38929
- fontWeight: 600,
38930
- lineHeight: 1.35,
38931
- marginBottom: 20
38932
39170
  }
38933
- }
38934
- ) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38935
- "div",
38936
- {
38937
- onClick: () => !issue.readonly && setEditingTitle(true),
38938
- style: {
38939
- marginBottom: 18,
38940
- color: GUI_THEME.primary,
38941
- fontSize: 18,
38942
- fontWeight: 600,
38943
- lineHeight: 1.35,
38944
- wordBreak: "break-word",
38945
- cursor: issue.readonly ? "default" : "text"
38946
- },
38947
- children: issue.title
38948
- }
38949
- ),
38950
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Tabs, { tabs, activeTab, onChange: onChangeTab }),
38951
- activeTab === "overview" && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
38952
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38953
- Section,
39171
+ ) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39172
+ "div",
38954
39173
  {
38955
- first: true,
38956
- title: "Description",
38957
- action: !issue.readonly && !editingDescription && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38958
- Button,
38959
- {
38960
- variant: "ghost",
38961
- onClick: () => setEditingDescription(true),
38962
- children: "edit"
38963
- }
38964
- ),
38965
- children: editingDescription ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
38966
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38967
- Textarea,
38968
- {
38969
- value: description,
38970
- autoFocus: true,
38971
- placeholder: "",
38972
- onChange: (event) => setDescription(event.target.value),
38973
- onKeyDown: (event) => {
38974
- if (event.key === "Escape") cancelDescription();
38975
- if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
38976
- saveDescription();
38977
- }
38978
- },
38979
- style: {
38980
- font: "inherit",
38981
- fontFamily: CONTENT_FONT,
38982
- fontSize: 13,
38983
- maxHeight: 320,
38984
- overflowY: "auto"
38985
- }
38986
- }
38987
- ),
38988
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(ActionRow, { children: [
38989
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { onClick: saveDescription, children: "save" }),
38990
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { variant: "ghost", onClick: cancelDescription, children: "cancel" })
38991
- ] })
38992
- ] }) : issue.description ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
38993
- "div",
38994
- {
38995
- style: {
38996
- marginTop: 8,
38997
- padding: "12px 16px",
38998
- maxHeight: 320,
38999
- overflowY: "auto",
39000
- background: GUI_THEME.tertiary,
39001
- borderRadius: 8
39002
- },
39003
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(MarkdownContent, { content: issue.description })
39004
- }
39005
- ) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Empty, { children: "No description" })
39174
+ onClick: () => !issue.readonly && setEditingTitle(true),
39175
+ style: {
39176
+ marginBottom: 18,
39177
+ color: GUI_THEME.primary,
39178
+ fontSize: 18,
39179
+ fontWeight: 600,
39180
+ lineHeight: 1.35,
39181
+ wordBreak: "break-word",
39182
+ cursor: issue.readonly ? "default" : "text"
39183
+ },
39184
+ children: issue.title
39006
39185
  }
39007
39186
  ),
39008
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
39009
- Section,
39010
- {
39011
- title: "Tags",
39012
- action: !issue.readonly && !addingTag && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { variant: "ghost", onClick: () => setAddingTag(true), children: "+" }) || addingTag && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { variant: "ghost", onClick: () => setAddingTag(false), children: "-" }),
39013
- children: [
39014
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ChipRow, { children: issue.tags.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Empty, { children: "No tags" }) : issue.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
39015
- Button,
39016
- {
39017
- variant: "chip",
39018
- disabled: issue.readonly,
39019
- onClick: () => onRemoveTag(issue.id, tag.id),
39020
- title: "Remove tag",
39021
- style: { color: tag.color },
39022
- children: [
39023
- tag.name,
39024
- " ",
39025
- !issue.readonly && "\xD7"
39026
- ]
39027
- },
39028
- tag.id
39029
- )) }),
39030
- addingTag && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
39187
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Tabs, { tabs, activeTab, onChange: onChangeTab }),
39188
+ activeTab === "overview" && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
39189
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39190
+ Section,
39191
+ {
39192
+ first: true,
39193
+ title: "Description",
39194
+ action: !issue.readonly && !editingDescription && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39031
39195
  Button,
39032
39196
  {
39033
- variant: "chip",
39034
- disabled: issue.readonly,
39035
- onClick: () => onAddTag(issue.id, tag.name),
39036
- title: "Add existing tag",
39037
- style: { color: tag.color, opacity: 0.55 },
39038
- children: [
39039
- "+ ",
39040
- tag.name
39041
- ]
39042
- },
39043
- tag.id
39044
- )) }),
39045
- addingTag && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(AddRow, { children: [
39046
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39047
- Input,
39197
+ variant: "ghost",
39198
+ onClick: () => setEditingDescription(true),
39199
+ children: "edit"
39200
+ }
39201
+ ),
39202
+ children: editingDescription ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
39203
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39204
+ Textarea,
39048
39205
  {
39049
- value: tagName,
39206
+ value: description,
39050
39207
  autoFocus: true,
39051
- placeholder: "tag name",
39052
- onChange: (event) => setTagName(event.target.value),
39208
+ placeholder: "",
39209
+ onChange: (event) => setDescription(event.target.value),
39053
39210
  onKeyDown: (event) => {
39054
- if (event.key === "Enter") addTag();
39055
- if (event.key === "Escape") {
39056
- setTagName("");
39057
- setAddingTag(false);
39211
+ if (event.key === "Escape") cancelDescription();
39212
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
39213
+ saveDescription();
39058
39214
  }
39215
+ },
39216
+ style: {
39217
+ font: "inherit",
39218
+ fontFamily: CONTENT_FONT,
39219
+ fontSize: 13,
39220
+ maxHeight: 320,
39221
+ overflowY: "auto"
39059
39222
  }
39060
39223
  }
39061
39224
  ),
39062
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { onClick: addTag, children: "add" })
39063
- ] })
39064
- ]
39065
- }
39066
- ),
39067
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
39068
- Section,
39069
- {
39070
- title: "Assignees",
39071
- action: !issue.readonly && !addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39072
- Button,
39073
- {
39074
- variant: "ghost",
39075
- onClick: () => setAddingAssignee(true),
39076
- children: "+"
39077
- }
39078
- ) || addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39079
- Button,
39080
- {
39081
- variant: "ghost",
39082
- onClick: () => setAddingAssignee(false),
39083
- children: "-"
39084
- }
39085
- ),
39086
- children: [
39087
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ChipRow, { children: issue.assignees.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Empty, { children: "No assignees" }) : issue.assignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
39225
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(ActionRow, { children: [
39226
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Button, { onClick: saveDescription, children: "save" }),
39227
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Button, { variant: "ghost", onClick: cancelDescription, children: "cancel" })
39228
+ ] })
39229
+ ] }) : issue.description ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39230
+ "div",
39231
+ {
39232
+ style: {
39233
+ marginTop: 8,
39234
+ padding: "12px 16px",
39235
+ maxHeight: 320,
39236
+ overflowY: "auto",
39237
+ background: GUI_THEME.tertiary,
39238
+ borderRadius: 8
39239
+ },
39240
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(MarkdownContent, { content: issue.description })
39241
+ }
39242
+ ) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Empty, { children: "No description" })
39243
+ }
39244
+ ),
39245
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39246
+ Section,
39247
+ {
39248
+ title: "Tags",
39249
+ action: !issue.readonly && !addingTag && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Button, { variant: "ghost", onClick: () => setAddingTag(true), children: "+" }) || addingTag && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Button, { variant: "ghost", onClick: () => setAddingTag(false), children: "-" }),
39250
+ children: [
39251
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ChipRow, { children: issue.tags.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Empty, { children: "No tags" }) : issue.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39252
+ Button,
39253
+ {
39254
+ variant: "chip",
39255
+ disabled: issue.readonly,
39256
+ onClick: () => onRemoveTag(issue.id, tag.id),
39257
+ title: "Remove tag",
39258
+ style: { color: tag.color },
39259
+ children: [
39260
+ tag.name,
39261
+ " ",
39262
+ !issue.readonly && "\xD7"
39263
+ ]
39264
+ },
39265
+ tag.id
39266
+ )) }),
39267
+ addingTag && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39268
+ Button,
39269
+ {
39270
+ variant: "chip",
39271
+ disabled: issue.readonly,
39272
+ onClick: () => onAddTag(issue.id, tag.name),
39273
+ title: "Add existing tag",
39274
+ style: { color: tag.color, opacity: 0.55 },
39275
+ children: [
39276
+ "+ ",
39277
+ tag.name
39278
+ ]
39279
+ },
39280
+ tag.id
39281
+ )) }),
39282
+ addingTag && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(AddRow, { children: [
39283
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39284
+ Input,
39285
+ {
39286
+ value: tagName,
39287
+ autoFocus: true,
39288
+ placeholder: "tag name",
39289
+ onChange: (event) => setTagName(event.target.value),
39290
+ onKeyDown: (event) => {
39291
+ if (event.key === "Enter") addTag();
39292
+ if (event.key === "Escape") {
39293
+ setTagName("");
39294
+ setAddingTag(false);
39295
+ }
39296
+ }
39297
+ }
39298
+ ),
39299
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Button, { onClick: addTag, children: "add" })
39300
+ ] })
39301
+ ]
39302
+ }
39303
+ ),
39304
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39305
+ Section,
39306
+ {
39307
+ title: "Assignees",
39308
+ action: !issue.readonly && !addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39088
39309
  Button,
39089
39310
  {
39090
- variant: "chip",
39091
- disabled: issue.readonly,
39092
- onClick: () => onRemoveAssignee(issue.id, assignee.id),
39093
- title: "Remove assignee",
39094
- style: { color: assignee.color },
39095
- children: [
39096
- "@",
39097
- assignee.name,
39098
- " ",
39099
- !issue.readonly && "\xD7"
39100
- ]
39101
- },
39102
- assignee.id
39103
- )) }),
39104
- addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ChipRow, { children: availableAssignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
39311
+ variant: "ghost",
39312
+ onClick: () => {
39313
+ setAddingAssignee(true);
39314
+ onOpenAssigneePicker();
39315
+ },
39316
+ children: "+"
39317
+ }
39318
+ ) || addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39105
39319
  Button,
39106
39320
  {
39107
- variant: "chip",
39108
- disabled: issue.readonly,
39109
- onClick: () => onAddAssignee(issue.id, assignee.name),
39110
- title: "Add existing assignee",
39111
- style: { color: assignee.color, opacity: 0.55 },
39112
- children: [
39113
- "+ @",
39114
- assignee.name
39115
- ]
39116
- },
39117
- assignee.id
39118
- )) }),
39119
- addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(AddRow, { children: [
39120
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39121
- Input,
39321
+ variant: "ghost",
39322
+ onClick: () => setAddingAssignee(false),
39323
+ children: "-"
39324
+ }
39325
+ ),
39326
+ children: [
39327
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ChipRow, { children: issue.assignees.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Empty, { children: "No assignees" }) : issue.assignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39328
+ Button,
39122
39329
  {
39123
- value: assigneeName,
39124
- autoFocus: true,
39125
- placeholder: "assignee name",
39126
- onChange: (event) => setAssigneeName(event.target.value),
39127
- onKeyDown: (event) => {
39128
- if (event.key === "Enter") addAssignee();
39129
- if (event.key === "Escape") {
39130
- setAssigneeName("");
39131
- setAddingAssignee(false);
39330
+ variant: "chip",
39331
+ disabled: issue.readonly,
39332
+ onClick: () => onRemoveAssignee(issue.id, assignee.id),
39333
+ title: "Remove assignee",
39334
+ style: { color: assignee.color },
39335
+ children: [
39336
+ "@",
39337
+ assignee.name,
39338
+ " ",
39339
+ !issue.readonly && "\xD7"
39340
+ ]
39341
+ },
39342
+ assignee.id
39343
+ )) }),
39344
+ addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ChipRow, { children: availableAssignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39345
+ Button,
39346
+ {
39347
+ variant: "chip",
39348
+ disabled: issue.readonly,
39349
+ onClick: () => onAddAssignee(issue.id, assignee.id),
39350
+ title: assignee.isSelf ? "Assign yourself" : !assignee.hasAuthoredAnywhere ? "Has not contributed to this project" : "Add existing assignee",
39351
+ style: {
39352
+ color: assignee.isSelf ? GUI_THEME.accent : assignee.color,
39353
+ opacity: assignee.isSelf ? 1 : 0.55,
39354
+ fontWeight: assignee.isSelf ? 600 : void 0,
39355
+ borderColor: assignee.isSelf ? GUI_THEME.accent : void 0
39356
+ },
39357
+ children: [
39358
+ "+ @",
39359
+ assignee.isSelf ? "me" : assignee.name,
39360
+ !assignee.hasAuthoredAnywhere ? " \u2197" : ""
39361
+ ]
39362
+ },
39363
+ assignee.id
39364
+ )) }),
39365
+ addingAssignee && assignees.some(
39366
+ (a) => !a.hasAuthoredAnywhere && !a.isRemoved
39367
+ ) && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ChipRow, { children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39368
+ Button,
39369
+ {
39370
+ variant: "ghost",
39371
+ disabled: issue.readonly,
39372
+ onClick: () => setManagingContributors(true),
39373
+ title: "Review and remove external contributors across the whole workspace",
39374
+ children: "manage contributors\u2026"
39375
+ }
39376
+ ) }),
39377
+ addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(AddRow, { children: [
39378
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39379
+ Input,
39380
+ {
39381
+ value: assigneeName,
39382
+ autoFocus: true,
39383
+ placeholder: "name of unknown contributor",
39384
+ onChange: (event) => setAssigneeName(event.target.value),
39385
+ onKeyDown: (event) => {
39386
+ if (event.key === "Enter") addAssignee();
39387
+ if (event.key === "Escape") {
39388
+ setAssigneeName("");
39389
+ setAddingAssignee(false);
39390
+ }
39132
39391
  }
39133
39392
  }
39134
- }
39135
- ),
39136
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { onClick: addAssignee, children: "add" })
39137
- ] })
39138
- ]
39139
- }
39140
- ),
39141
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39142
- IssueAttachments,
39393
+ ),
39394
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39395
+ Button,
39396
+ {
39397
+ onClick: addAssignee,
39398
+ title: "Add someone who has not contributed to this board",
39399
+ children: "add"
39400
+ }
39401
+ )
39402
+ ] })
39403
+ ]
39404
+ }
39405
+ ),
39406
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39407
+ IssueAttachments,
39408
+ {
39409
+ issueId: issue.id,
39410
+ readonly: Boolean(issue.readonly),
39411
+ attachments,
39412
+ uploadStatus: attachmentUploadStatus,
39413
+ onUploadFiles: onUploadAttachments,
39414
+ onDeleteAttachment
39415
+ }
39416
+ ),
39417
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39418
+ Section,
39419
+ {
39420
+ title: "Actions",
39421
+ action: !issue.readonly && (issue.isClosed ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Button, { onClick: () => onReopenIssue(issue.id), children: "reopen issue" }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Button, { onClick: () => onCloseIssue(issue.id), children: "close issue" })),
39422
+ children: ""
39423
+ }
39424
+ )
39425
+ ] }),
39426
+ activeTab === "comments" && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39427
+ IssueComments,
39143
39428
  {
39429
+ whoAmI,
39144
39430
  issueId: issue.id,
39145
- readonly: Boolean(issue.readonly),
39146
- attachments,
39147
- uploadStatus: attachmentUploadStatus,
39148
- onUploadFiles: onUploadAttachments,
39149
- onDeleteAttachment
39150
- }
39151
- ),
39152
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39153
- Section,
39154
- {
39155
- title: "Actions",
39156
- action: issue.isClosed ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { onClick: () => onReopenIssue(issue.id), children: "reopen issue" }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Button, { onClick: () => onCloseIssue(issue.id), children: "close issue" }),
39157
- children: ""
39431
+ readonly: issue.readonly,
39432
+ comments,
39433
+ onAddComment,
39434
+ onDeleteComment
39158
39435
  }
39159
39436
  )
39160
- ] }),
39161
- activeTab === "comments" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39162
- IssueComments,
39437
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Empty, { children: "Select an issue" }),
39438
+ managingContributors && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39439
+ ManageContributorsModal,
39163
39440
  {
39164
- whoAmI,
39165
- issueId: issue.id,
39166
- readonly: issue.readonly,
39167
- comments,
39168
- onAddComment,
39169
- onDeleteComment
39441
+ contributors: assignees,
39442
+ onRemove: onRemoveContributor,
39443
+ onClose: () => setManagingContributors(false)
39170
39444
  }
39171
39445
  )
39172
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Empty, { children: "Select an issue" }) });
39446
+ ] });
39173
39447
  };
39174
39448
 
39175
39449
  // source/gui/client/components/SwimlaneColumn.tsx
39176
- var import_react10 = __toESM(require_react(), 1);
39450
+ var import_react11 = __toESM(require_react(), 1);
39177
39451
 
39178
39452
  // source/gui/client/components/IconComment.tsx
39179
- var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
39180
- var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
39453
+ var import_jsx_runtime21 = __toESM(require_jsx_runtime(), 1);
39454
+ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
39181
39455
  "svg",
39182
39456
  {
39183
39457
  width: size,
@@ -39185,7 +39459,7 @@ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime18.js
39185
39459
  viewBox: "0 0 16 16",
39186
39460
  fill: "none",
39187
39461
  "aria-hidden": "true",
39188
- children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
39462
+ children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
39189
39463
  "path",
39190
39464
  {
39191
39465
  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",
@@ -39200,7 +39474,7 @@ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime18.js
39200
39474
  );
39201
39475
 
39202
39476
  // source/gui/client/components/TicketCard.tsx
39203
- var import_jsx_runtime19 = __toESM(require_jsx_runtime(), 1);
39477
+ var import_jsx_runtime22 = __toESM(require_jsx_runtime(), 1);
39204
39478
  var TicketCard = ({
39205
39479
  ticket,
39206
39480
  index: index2,
@@ -39213,7 +39487,7 @@ var TicketCard = ({
39213
39487
  onDropIssueAt
39214
39488
  }) => {
39215
39489
  const getVisualTargetIndex = (isAfterMiddle) => index2 + (isAfterMiddle ? 1 : 0);
39216
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
39490
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
39217
39491
  "div",
39218
39492
  {
39219
39493
  draggable: !ticket.readonly,
@@ -39254,7 +39528,7 @@ var TicketCard = ({
39254
39528
  border: `1px solid ${isSelected ? GUI_THEME.accent : "transparent"}`
39255
39529
  },
39256
39530
  children: [
39257
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
39531
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39258
39532
  "div",
39259
39533
  {
39260
39534
  style: {
@@ -39268,7 +39542,7 @@ var TicketCard = ({
39268
39542
  children: isSelected ? "\u276F" : index2 + 1
39269
39543
  }
39270
39544
  ),
39271
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
39545
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
39272
39546
  "div",
39273
39547
  {
39274
39548
  style: {
@@ -39280,7 +39554,7 @@ var TicketCard = ({
39280
39554
  paddingLeft: 8
39281
39555
  },
39282
39556
  children: [
39283
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
39557
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
39284
39558
  "div",
39285
39559
  {
39286
39560
  style: {
@@ -39291,7 +39565,7 @@ var TicketCard = ({
39291
39565
  flex: 1
39292
39566
  },
39293
39567
  children: [
39294
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
39568
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39295
39569
  "div",
39296
39570
  {
39297
39571
  style: {
@@ -39307,17 +39581,17 @@ var TicketCard = ({
39307
39581
  children: ticket.title
39308
39582
  }
39309
39583
  ),
39310
- ticket.ref && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
39584
+ ticket.ref && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39311
39585
  "div",
39312
39586
  {
39313
39587
  style: {
39314
39588
  color: GUI_THEME.dim2,
39315
39589
  fontSize: 10
39316
39590
  },
39317
- children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(CopyRef, { refValue: ticket.ref })
39591
+ children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(CopyRef, { refValue: ticket.ref })
39318
39592
  }
39319
39593
  ),
39320
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
39594
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39321
39595
  "div",
39322
39596
  {
39323
39597
  style: {
@@ -39326,7 +39600,7 @@ var TicketCard = ({
39326
39600
  gap: 6,
39327
39601
  alignItems: "center"
39328
39602
  },
39329
- children: ticket.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
39603
+ children: ticket.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39330
39604
  "span",
39331
39605
  {
39332
39606
  style: {
@@ -39346,7 +39620,7 @@ var TicketCard = ({
39346
39620
  ]
39347
39621
  }
39348
39622
  ),
39349
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
39623
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39350
39624
  "div",
39351
39625
  {
39352
39626
  style: {
@@ -39356,7 +39630,7 @@ var TicketCard = ({
39356
39630
  flexShrink: 0,
39357
39631
  paddingTop: 2
39358
39632
  },
39359
- children: commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
39633
+ children: commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
39360
39634
  "button",
39361
39635
  {
39362
39636
  type: "button",
@@ -39394,14 +39668,14 @@ var TicketCard = ({
39394
39668
  event.currentTarget.style.transform = "translateY(0)";
39395
39669
  },
39396
39670
  children: [
39397
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(IconComment, {}),
39398
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { children: commentCount })
39671
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(IconComment, {}),
39672
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("span", { children: commentCount })
39399
39673
  ]
39400
39674
  }
39401
39675
  )
39402
39676
  }
39403
39677
  ),
39404
- ticket.assignees.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
39678
+ ticket.assignees.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
39405
39679
  "div",
39406
39680
  {
39407
39681
  style: {
@@ -39411,7 +39685,7 @@ var TicketCard = ({
39411
39685
  paddingTop: 2,
39412
39686
  marginTop: "-4px"
39413
39687
  },
39414
- children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(User, { user: assignee, index: idx, isFocus: isSelected }))
39688
+ children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(User, { user: assignee, index: idx, isFocus: isSelected }))
39415
39689
  }
39416
39690
  )
39417
39691
  ]
@@ -39423,7 +39697,10 @@ var TicketCard = ({
39423
39697
  };
39424
39698
 
39425
39699
  // source/gui/client/components/SwimlaneColumn.tsx
39426
- var import_jsx_runtime20 = __toESM(require_jsx_runtime(), 1);
39700
+ var import_jsx_runtime23 = __toESM(require_jsx_runtime(), 1);
39701
+ var COLUMN_GLOW_COLOR = "rgb(140, 176, 232)";
39702
+ var COLUMN_PADDING = 14;
39703
+ var SCROLLBAR_GUTTER_INSET = COLUMN_PADDING / 2;
39427
39704
  var SwimlaneColumn = ({
39428
39705
  swimlane,
39429
39706
  selected,
@@ -39439,20 +39716,27 @@ var SwimlaneColumn = ({
39439
39716
  onDragOverIssue,
39440
39717
  onDragLeave
39441
39718
  }) => {
39442
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39719
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
39443
39720
  Panel,
39444
39721
  {
39445
39722
  as: "section",
39446
39723
  active: dragOver,
39447
39724
  borderColor: selected || dragOver ? GUI_THEME.accent : GUI_THEME.line,
39448
- glowOpacity: 0.15,
39725
+ glowColor: COLUMN_GLOW_COLOR,
39726
+ glowOpacity: 0.41,
39727
+ glowRadius: 370,
39728
+ proximityReach: 200,
39449
39729
  style: {
39450
39730
  zIndex: 0,
39451
39731
  width: 360,
39452
39732
  minWidth: 360,
39453
- height: "calc(100vh - 160px)",
39733
+ // Fills the board row rather than guessing at the chrome above with a
39734
+ // viewport calc; the scrubber's height changes as it is used.
39735
+ height: "100%",
39736
+ // Panel's 1px border would otherwise add to the 100% and overflow the row.
39737
+ boxSizing: "border-box",
39454
39738
  background: dragOver ? "#14202a" : GUI_THEME.bg,
39455
- padding: "0 14px",
39739
+ padding: `0 ${COLUMN_PADDING}px`,
39456
39740
  display: "flex",
39457
39741
  flexDirection: "column"
39458
39742
  },
@@ -39479,7 +39763,7 @@ var SwimlaneColumn = ({
39479
39763
  onDragLeave();
39480
39764
  },
39481
39765
  children: [
39482
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39766
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
39483
39767
  "header",
39484
39768
  {
39485
39769
  style: {
@@ -39491,7 +39775,7 @@ var SwimlaneColumn = ({
39491
39775
  alignItems: "center"
39492
39776
  },
39493
39777
  children: [
39494
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39778
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
39495
39779
  "div",
39496
39780
  {
39497
39781
  style: {
@@ -39500,30 +39784,37 @@ var SwimlaneColumn = ({
39500
39784
  gap: 8
39501
39785
  },
39502
39786
  children: [
39503
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39787
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39504
39788
  "span",
39505
39789
  {
39506
39790
  style: { color: selected ? GUI_THEME.accent : GUI_THEME.secondary },
39507
39791
  children: selected ? "\u276F" : " "
39508
39792
  }
39509
39793
  ),
39510
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39794
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39511
39795
  "strong",
39512
39796
  {
39513
39797
  style: { color: selected ? GUI_THEME.accent : GUI_THEME.primary },
39514
39798
  children: swimlane.title
39515
39799
  }
39516
39800
  ),
39517
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("span", { style: { color: GUI_THEME.dim }, children: [
39801
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)("span", { style: { color: GUI_THEME.dim }, children: [
39518
39802
  "(",
39519
39803
  swimlane.issues.length,
39520
39804
  ")"
39521
39805
  ] }),
39522
- swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { children: "\u{1F512}" })
39806
+ swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39807
+ "span",
39808
+ {
39809
+ title: "Read-only",
39810
+ style: { display: "flex", color: GUI_THEME.dim },
39811
+ children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(IconLock, {})
39812
+ }
39813
+ )
39523
39814
  ]
39524
39815
  }
39525
39816
  ),
39526
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39817
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39527
39818
  Button,
39528
39819
  {
39529
39820
  variant: "ghost",
@@ -39536,48 +39827,1602 @@ var SwimlaneColumn = ({
39536
39827
  ]
39537
39828
  }
39538
39829
  ),
39539
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { style: { overflow: "auto", paddingTop: 4, flex: 1, minHeight: 0 }, children: swimlane.issues.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
39540
- dropIndex === 0 && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(DropIndicator, {}),
39541
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39542
- "div",
39543
- {
39544
- style: {
39545
- padding: 24,
39546
- textAlign: "center",
39547
- color: GUI_THEME.dim,
39548
- fontSize: 12
39549
- },
39550
- children: "Drop issue here"
39551
- }
39552
- )
39553
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
39554
- swimlane.issues.map((ticket, index2) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_react10.default.Fragment, { children: [
39555
- dropIndex === index2 && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(DropIndicator, {}),
39556
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39557
- TicketCard,
39558
- {
39559
- ticket,
39560
- index: index2,
39561
- isSelected: ticket.id === selectedIssueId,
39562
- onSelect: () => onSelectIssue(ticket.id),
39563
- onDragStart: (issueId) => onSelectIssue(issueId),
39564
- onOpenComments: onSelectIssueComments,
39565
- commentCount: commentsByIssueId[ticket.id].length ?? 0,
39566
- onDragOverIssue: (targetIndex) => onDragOverIssue(swimlane.id, targetIndex),
39567
- onDropIssueAt: (issueId, targetIndex) => {
39568
- onDropIssue(issueId, swimlane.id, targetIndex);
39569
- onDragLeave();
39830
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39831
+ "div",
39832
+ {
39833
+ style: {
39834
+ overflow: "auto",
39835
+ paddingTop: 4,
39836
+ marginRight: -SCROLLBAR_GUTTER_INSET,
39837
+ paddingRight: SCROLLBAR_GUTTER_INSET,
39838
+ flex: 1,
39839
+ minHeight: 0
39840
+ },
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)(
39844
+ "div",
39845
+ {
39846
+ style: {
39847
+ padding: 24,
39848
+ textAlign: "center",
39849
+ color: GUI_THEME.dim,
39850
+ fontSize: 12
39851
+ },
39852
+ children: "Drop issue here"
39570
39853
  }
39571
- }
39572
- )
39573
- ] }, ticket.id)),
39574
- dropIndex === swimlane.issues.length && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(DropIndicator, {})
39575
- ] }) })
39854
+ )
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)(
39859
+ TicketCard,
39860
+ {
39861
+ ticket,
39862
+ index: index2,
39863
+ isSelected: ticket.id === selectedIssueId,
39864
+ onSelect: () => onSelectIssue(ticket.id),
39865
+ onDragStart: (issueId) => onSelectIssue(issueId),
39866
+ onOpenComments: onSelectIssueComments,
39867
+ commentCount: commentsByIssueId[ticket.id]?.length ?? 0,
39868
+ onDragOverIssue: (targetIndex) => onDragOverIssue(swimlane.id, targetIndex),
39869
+ onDropIssueAt: (issueId, targetIndex) => {
39870
+ onDropIssue(issueId, swimlane.id, targetIndex);
39871
+ onDragLeave();
39872
+ }
39873
+ }
39874
+ )
39875
+ ] }, ticket.id)),
39876
+ dropIndex === swimlane.issues.length && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(DropIndicator, {})
39877
+ ] })
39878
+ }
39879
+ )
39880
+ ]
39881
+ }
39882
+ );
39883
+ };
39884
+
39885
+ // source/gui/client/components/GlobalScrollbarStyles.tsx
39886
+ var import_jsx_runtime24 = __toESM(require_jsx_runtime(), 1);
39887
+ var SCROLLBAR_SIZE = 3;
39888
+ var GlobalScrollbarStyles = () => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("style", { children: `
39889
+ /* Gated deliberately: Chrome implements scrollbar-width/-color too, and
39890
+ setting either makes it ignore the ::-webkit-scrollbar rules below. */
39891
+ @supports not selector(::-webkit-scrollbar) {
39892
+ * {
39893
+ scrollbar-width: thin;
39894
+ scrollbar-color: ${GUI_THEME.dim} transparent;
39895
+ }
39896
+ }
39897
+
39898
+ ::-webkit-scrollbar {
39899
+ width: ${SCROLLBAR_SIZE}px;
39900
+ height: ${SCROLLBAR_SIZE}px;
39901
+ }
39902
+
39903
+ /* Transparent, or the track draws a permanent stripe down every edge. */
39904
+ ::-webkit-scrollbar-track {
39905
+ background: transparent;
39906
+ }
39907
+
39908
+ ::-webkit-scrollbar-thumb {
39909
+ background: ${GUI_THEME.dim};
39910
+ border-radius: 999px;
39911
+ /* Insets the thumb below the track's hit target; 2px here would leave
39912
+ it 2px wide and lost against the panel. */
39913
+ border: 1px solid transparent;
39914
+ background-clip: content-box;
39915
+ }
39916
+
39917
+ ::-webkit-scrollbar-thumb:hover {
39918
+ background: ${GUI_THEME.secondary};
39919
+ background-clip: content-box;
39920
+ }
39921
+
39922
+ /* Unpainted, or the corner shows as a lighter square. */
39923
+ ::-webkit-scrollbar-corner {
39924
+ background: transparent;
39925
+ }
39926
+ ` });
39927
+
39928
+ // 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)(
39931
+ "div",
39932
+ {
39933
+ style: {
39934
+ position: "fixed",
39935
+ bottom: 20,
39936
+ right: 20,
39937
+ zIndex: 1e3,
39938
+ maxWidth: 360,
39939
+ display: "flex",
39940
+ alignItems: "flex-start",
39941
+ gap: 8,
39942
+ fontSize: 12,
39943
+ color: GUI_THEME.primary,
39944
+ background: GUI_THEME.panel,
39945
+ border: `1px solid ${GUI_THEME.red}`,
39946
+ borderRadius: 8,
39947
+ padding: "10px 12px",
39948
+ boxShadow: "0 4px 16px rgba(0, 0, 0, 0.4)"
39949
+ },
39950
+ 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)(
39953
+ "button",
39954
+ {
39955
+ onClick: onDismiss,
39956
+ style: {
39957
+ background: "transparent",
39958
+ border: "none",
39959
+ color: GUI_THEME.dim,
39960
+ cursor: "pointer",
39961
+ fontSize: 14,
39962
+ lineHeight: 1,
39963
+ padding: 0
39964
+ },
39965
+ children: "\xD7"
39966
+ }
39967
+ )
39968
+ ]
39969
+ }
39970
+ );
39971
+
39972
+ // source/gui/client/components/TimeScrubber.tsx
39973
+ var import_react14 = __toESM(require_react(), 1);
39974
+
39975
+ // source/lib/utils/date.utils.ts
39976
+ var pad = (n) => String(n).padStart(2, "0");
39977
+ var formatDateTime = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
39978
+ var formatTimeOfDay = (date) => `${pad(date.getHours())}:${pad(date.getMinutes())}`;
39979
+ var isSameDay = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
39980
+
39981
+ // source/lib/utils/minmax.ts
39982
+ var minOf = (values, seed) => values.reduce((min, value) => value < min ? value : min, seed);
39983
+ var maxOf = (values, seed) => values.reduce((max, value) => value > max ? value : max, seed);
39984
+
39985
+ // source/gui/client/lib/scrubber.ts
39986
+ var import_react12 = __toESM(require_react(), 1);
39987
+ var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
39988
+ var TRACK_HEIGHT = 24;
39989
+ var EVENTS_MODE_TOTAL_HEIGHT = 8 + TRACK_HEIGHT * 2;
39990
+ var EVENTS_SCATTER_HEIGHT = TRACK_HEIGHT + 16;
39991
+ var EVENTS_MODE_VERTICAL_PADDING = (EVENTS_MODE_TOTAL_HEIGHT - EVENTS_SCATTER_HEIGHT) / 2;
39992
+ var HOVER_HINT_WIDTH = 220;
39993
+ var SEGMENT_HIGHLIGHT_COLOR = "rgba(122, 158, 214, 0.07)";
39994
+ var BUCKET_HIGHLIGHT_COLOR = "rgba(255, 255, 255, 0.06)";
39995
+ var NEEDLE_COLOR = "rgba(255, 255, 255, 0.62)";
39996
+ var MIN_BUCKET_COUNT_FOR_GAP = 300;
39997
+ var barWidthCss = (bucketCount) => bucketCount < MIN_BUCKET_COUNT_FOR_GAP ? `calc(${100 / bucketCount}% - 1px)` : `${100 / bucketCount}%`;
39998
+ var DAY_MS = 24 * 60 * 60 * 1e3;
39999
+ var MIN_TIME_BUCKETS = 60;
40000
+ var MAX_TIME_BUCKETS = 900;
40001
+ var bucketCountForSpan = (spanMs) => {
40002
+ const days = Math.max(1, spanMs / DAY_MS);
40003
+ return Math.round(
40004
+ clamp(116.5 * Math.pow(days, 0.3265), MIN_TIME_BUCKETS, MAX_TIME_BUCKETS)
40005
+ );
40006
+ };
40007
+ var buildAxis = (timeline, commits, now = Date.now()) => {
40008
+ const commitBounds = commits.length ? commits.map((c) => c.time) : [now];
40009
+ const earliest = minOf(
40010
+ commitBounds,
40011
+ timeline?.buckets[0]?.t ?? timeline?.earliest ?? now
40012
+ );
40013
+ const latest = maxOf(commitBounds, timeline?.latest ?? now);
40014
+ const span = Math.max(1, latest - earliest);
40015
+ const bucketCount = bucketCountForSpan(span);
40016
+ const bucketMs = span / bucketCount;
40017
+ return {
40018
+ earliest,
40019
+ latest,
40020
+ span,
40021
+ bucketCount,
40022
+ bucketMs,
40023
+ bucketIndexForTime: (time) => clamp(Math.floor((time - earliest) / bucketMs), 0, bucketCount - 1),
40024
+ bucketTimeAt: (index2) => earliest + index2 * bucketMs,
40025
+ fractionForTime: (time) => clamp((time - earliest) / span, 0, 1),
40026
+ fractionToTime: (fraction) => Math.round(earliest + clamp(fraction, 0, 1) * span)
40027
+ };
40028
+ };
40029
+ var bucketIssueCounts = (axis, timeline) => {
40030
+ const counts = new Array(axis.bucketCount).fill(0);
40031
+ for (const sparseBucket of timeline?.buckets ?? []) {
40032
+ counts[axis.bucketIndexForTime(sparseBucket.t)] += sparseBucket.count;
40033
+ }
40034
+ return counts;
40035
+ };
40036
+ var bucketCommitStats = (axis, commits) => {
40037
+ const byIndex = /* @__PURE__ */ new Map();
40038
+ for (const commit of commits) {
40039
+ const index2 = axis.bucketIndexForTime(commit.time);
40040
+ const existing = byIndex.get(index2) ?? { count: 0, linesChanged: 0 };
40041
+ byIndex.set(index2, {
40042
+ count: existing.count + 1,
40043
+ linesChanged: existing.linesChanged + commit.linesChanged
40044
+ });
40045
+ }
40046
+ return byIndex;
40047
+ };
40048
+ var populatedRange = (bars) => bars.length > 0 ? [
40049
+ bars.reduce((min, bar) => Math.min(min, bar.index), Infinity),
40050
+ bars.reduce((max, bar) => Math.max(max, bar.index), -Infinity)
40051
+ ] : [0, 0];
40052
+ var hourFractionForTime = (time) => {
40053
+ const date = new Date(time);
40054
+ return (date.getHours() * 60 + date.getMinutes()) / (24 * 60);
40055
+ };
40056
+ var SEGMENT_UNIT_ORDER = ["day", "week", "month", "year"];
40057
+ var APPROX_UNIT_MS = {
40058
+ day: 24 * 60 * 60 * 1e3,
40059
+ week: 7 * 24 * 60 * 60 * 1e3,
40060
+ month: 30.44 * 24 * 60 * 60 * 1e3,
40061
+ year: 365.25 * 24 * 60 * 60 * 1e3
40062
+ };
40063
+ var MAX_SEGMENTS = 35;
40064
+ var chooseSegmentUnit = (spanMs) => SEGMENT_UNIT_ORDER.find(
40065
+ (unit) => spanMs / APPROX_UNIT_MS[unit] <= MAX_SEGMENTS
40066
+ ) ?? "year";
40067
+ var WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
40068
+ var MONTH_LABELS = [
40069
+ "Jan",
40070
+ "Feb",
40071
+ "Mar",
40072
+ "Apr",
40073
+ "May",
40074
+ "Jun",
40075
+ "Jul",
40076
+ "Aug",
40077
+ "Sep",
40078
+ "Oct",
40079
+ "Nov",
40080
+ "Dec"
40081
+ ];
40082
+ var advanceByUnit = (date, unit) => {
40083
+ if (unit === "day") date.setDate(date.getDate() + 1);
40084
+ else if (unit === "week") date.setDate(date.getDate() + 7);
40085
+ else if (unit === "month") date.setMonth(date.getMonth() + 1);
40086
+ else date.setFullYear(date.getFullYear() + 1);
40087
+ };
40088
+ var segmentAt = (time, unit) => {
40089
+ const start2 = new Date(time);
40090
+ start2.setHours(0, 0, 0, 0);
40091
+ if (unit === "week") {
40092
+ start2.setDate(start2.getDate() - (start2.getDay() + 6) % 7);
40093
+ } else if (unit === "month") {
40094
+ start2.setDate(1);
40095
+ } else if (unit === "year") {
40096
+ start2.setMonth(0, 1);
40097
+ }
40098
+ const end = new Date(start2);
40099
+ advanceByUnit(end, unit);
40100
+ 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()}`;
40102
+ return { start: start2.getTime(), end: end.getTime(), label };
40103
+ };
40104
+ var formatInterval = (start2, end) => {
40105
+ const startDate = new Date(start2);
40106
+ const endDate = new Date(end);
40107
+ const endLabel = isSameDay(startDate, endDate) ? formatTimeOfDay(endDate) : formatDateTime(endDate);
40108
+ return `${formatDateTime(startDate)} \u2013 ${endLabel}`;
40109
+ };
40110
+ var SCOPES = ["week", "month", "year", "all"];
40111
+ var SCOPE_DURATION_MS = {
40112
+ week: 7 * 24 * 60 * 60 * 1e3,
40113
+ month: 30 * 24 * 60 * 60 * 1e3,
40114
+ year: 365 * 24 * 60 * 60 * 1e3
40115
+ };
40116
+ var isScope = (value) => SCOPES.includes(value ?? "");
40117
+ var getPeriodRange = (scope, offset) => {
40118
+ if (scope === "all") return null;
40119
+ const durationMs = SCOPE_DURATION_MS[scope];
40120
+ const end = Date.now() - offset * durationMs;
40121
+ return { start: end - durationMs, end };
40122
+ };
40123
+ var formatPeriodLabel = (scope, offset, range) => {
40124
+ 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
+ }
40128
+ const start2 = new Date(range.start);
40129
+ const end = new Date(range.end);
40130
+ return `${start2.getMonth() + 1}/${start2.getDate()} \u2013 ${end.getMonth() + 1}/${end.getDate()}`;
40131
+ };
40132
+ var scopeButtonLabel = (scope) => scope === "all" ? "All" : scope[0].toUpperCase() + scope.slice(1);
40133
+ var hashUnitInterval = (key) => {
40134
+ let hash = 2166136261;
40135
+ for (let index2 = 0; index2 < key.length; index2++) {
40136
+ hash ^= key.charCodeAt(index2);
40137
+ hash = Math.imul(hash, 16777619);
40138
+ }
40139
+ return (hash >>> 0) % 1e4 / 1e4;
40140
+ };
40141
+ var DOT_APPEAR_MS = 260;
40142
+ var DOT_APPEAR_SCATTER_MS = 620;
40143
+ 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`;
40148
+ var DOT_EXIT_TOTAL_MS = DOT_APPEAR_SCATTER_MS + DOT_APPEAR_MS;
40149
+ var BAR_GROW_MS = 200;
40150
+ var BAR_GROW_SWEEP_MS = 560;
40151
+ var barGrowAnimation = (index2, firstIndex, lastIndex) => {
40152
+ const span = lastIndex - firstIndex;
40153
+ const delay = span > 0 ? (index2 - firstIndex) / span * BAR_GROW_SWEEP_MS : 0;
40154
+ return `epiqScrubberGrow ${BAR_GROW_MS}ms ease-out ${delay.toFixed(
40155
+ 0
40156
+ )}ms backwards`;
40157
+ };
40158
+ var FADE_IN_ANIMATION = "epiqScrubberFadeIn 320ms ease-out";
40159
+ var SCRUBBER_KEYFRAMES = `
40160
+ /* Must animate the standalone 'scale' property, not 'transform': the dots
40161
+ carry a 'transform: translate(...)' to centre themselves, and animating
40162
+ 'transform' would replace it and fling them off position. */
40163
+ @keyframes epiqScrubberTwinkle {
40164
+ from { scale: 0; }
40165
+ to { scale: 1; }
40166
+ }
40167
+
40168
+ /* Not the twinkle reversed \u2014 see dotExitAnimation for why. */
40169
+ @keyframes epiqScrubberRetract {
40170
+ from { scale: 1; }
40171
+ to { scale: 0; }
40172
+ }
40173
+
40174
+ @keyframes epiqScrubberGrow {
40175
+ from { transform: scaleY(0); }
40176
+ to { transform: scaleY(1); }
40177
+ }
40178
+
40179
+ @keyframes epiqScrubberFadeIn {
40180
+ /* Starts faint rather than transparent: from zero the whole chart reads
40181
+ as blinking on a mere data refresh. */
40182
+ from { opacity: 0.2; }
40183
+ to { opacity: 1; }
40184
+ }
40185
+ `;
40186
+ var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
40187
+ var usePrefersReducedMotion = () => {
40188
+ const [reduced, setReduced] = (0, import_react12.useState)(
40189
+ () => window.matchMedia(REDUCED_MOTION_QUERY).matches
40190
+ );
40191
+ (0, import_react12.useEffect)(() => {
40192
+ const query = window.matchMedia(REDUCED_MOTION_QUERY);
40193
+ const onChange = () => setReduced(query.matches);
40194
+ query.addEventListener("change", onChange);
40195
+ return () => query.removeEventListener("change", onChange);
40196
+ }, []);
40197
+ return reduced;
40198
+ };
40199
+ 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);
40204
+ duration.current = durationMs;
40205
+ (0, import_react12.useEffect)(() => {
40206
+ if (visible === wasVisible.current) return;
40207
+ wasVisible.current = visible;
40208
+ if (visible) {
40209
+ setLeaving(false);
40210
+ setMounted(true);
40211
+ return;
40212
+ }
40213
+ if (duration.current === 0) {
40214
+ setMounted(false);
40215
+ return;
40216
+ }
40217
+ setLeaving(true);
40218
+ const timeout = setTimeout(() => {
40219
+ setLeaving(false);
40220
+ setMounted(false);
40221
+ }, duration.current);
40222
+ return () => clearTimeout(timeout);
40223
+ }, [visible]);
40224
+ return { mounted, leaving };
40225
+ };
40226
+ var usePersistedFlag = (key, fallback) => {
40227
+ const [value, setValue] = (0, import_react12.useState)(() => {
40228
+ const stored = localStorage.getItem(key);
40229
+ return stored === null ? fallback : stored === "true";
40230
+ });
40231
+ return [
40232
+ value,
40233
+ (next) => {
40234
+ setValue(next);
40235
+ localStorage.setItem(key, String(next));
40236
+ }
40237
+ ];
40238
+ };
40239
+
40240
+ // source/gui/client/components/ScrubberParts.tsx
40241
+ var import_react13 = __toESM(require_react(), 1);
40242
+
40243
+ // source/gui/client/components/Checkbox.tsx
40244
+ var import_jsx_runtime26 = __toESM(require_jsx_runtime(), 1);
40245
+ var Checkbox = ({
40246
+ label,
40247
+ checked,
40248
+ onChange,
40249
+ activeColor = GUI_THEME.accent,
40250
+ disabled,
40251
+ title
40252
+ }) => {
40253
+ const color2 = checked ? activeColor : GUI_THEME.dim;
40254
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
40255
+ "label",
40256
+ {
40257
+ title,
40258
+ style: {
40259
+ display: "flex",
40260
+ alignItems: "center",
40261
+ gap: 5,
40262
+ fontSize: 10,
40263
+ color: color2,
40264
+ cursor: disabled ? "not-allowed" : "pointer",
40265
+ opacity: disabled ? 0.4 : 1
40266
+ },
40267
+ children: [
40268
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
40269
+ "span",
40270
+ {
40271
+ style: {
40272
+ position: "relative",
40273
+ display: "inline-flex",
40274
+ alignItems: "center",
40275
+ justifyContent: "center",
40276
+ width: 12,
40277
+ height: 12,
40278
+ // Smaller than the buttons' 6px: at 12px square, 6 rounds to a circle.
40279
+ borderRadius: 4,
40280
+ border: `1px solid ${color2}`,
40281
+ background: "transparent",
40282
+ flexShrink: 0
40283
+ },
40284
+ children: [
40285
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
40286
+ "input",
40287
+ {
40288
+ type: "checkbox",
40289
+ checked,
40290
+ disabled,
40291
+ onChange: (event) => onChange(event.target.checked),
40292
+ style: {
40293
+ position: "absolute",
40294
+ inset: 0,
40295
+ width: "100%",
40296
+ height: "100%",
40297
+ margin: 0,
40298
+ opacity: 0,
40299
+ cursor: "inherit"
40300
+ }
40301
+ }
40302
+ ),
40303
+ checked && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
40304
+ "span",
40305
+ {
40306
+ style: {
40307
+ width: 6,
40308
+ height: 6,
40309
+ borderRadius: 1,
40310
+ background: activeColor
40311
+ }
40312
+ }
40313
+ )
40314
+ ]
40315
+ }
40316
+ ),
40317
+ label
40318
+ ]
40319
+ }
40320
+ );
40321
+ };
40322
+
40323
+ // 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)(
40326
+ "svg",
40327
+ {
40328
+ width: size,
40329
+ height: size,
40330
+ viewBox: "0 0 24 24",
40331
+ fill: "none",
40332
+ stroke: "currentColor",
40333
+ strokeWidth: "2",
40334
+ strokeLinecap: "round",
40335
+ strokeLinejoin: "round",
40336
+ "aria-hidden": "true",
40337
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("polyline", { points: "6 9 12 15 18 9" })
40338
+ }
40339
+ );
40340
+
40341
+ // 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)(
40344
+ "svg",
40345
+ {
40346
+ width: size,
40347
+ height: size,
40348
+ viewBox: "0 0 24 24",
40349
+ fill: "none",
40350
+ stroke: "currentColor",
40351
+ strokeWidth: "2",
40352
+ strokeLinecap: "round",
40353
+ strokeLinejoin: "round",
40354
+ "aria-hidden": "true",
40355
+ children: /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("polyline", { points: "9 18 15 12 9 6" })
40356
+ }
40357
+ );
40358
+
40359
+ // source/gui/client/components/ScrubberParts.tsx
40360
+ var import_jsx_runtime29 = __toESM(require_jsx_runtime(), 1);
40361
+ var toggleButtonStyle = (active) => ({
40362
+ background: "transparent",
40363
+ border: `1px solid ${active ? GUI_THEME.accent : GUI_THEME.dim}`,
40364
+ color: active ? GUI_THEME.accent : GUI_THEME.dim,
40365
+ borderRadius: 6,
40366
+ fontSize: 10,
40367
+ padding: "2px 8px",
40368
+ cursor: "pointer"
40369
+ });
40370
+ var navButtonStyle = {
40371
+ background: "transparent",
40372
+ border: `1px solid ${GUI_THEME.dim}`,
40373
+ color: GUI_THEME.dim,
40374
+ borderRadius: 6,
40375
+ fontSize: 10,
40376
+ padding: "2px 6px",
40377
+ cursor: "pointer",
40378
+ lineHeight: 1
40379
+ };
40380
+ var ScrubberControls = ({
40381
+ scope,
40382
+ offset,
40383
+ periodRange,
40384
+ layoutMode,
40385
+ showIssues,
40386
+ showCommits,
40387
+ allBoards,
40388
+ isScrubbing,
40389
+ nowLabel,
40390
+ onChangeScope,
40391
+ onChangeOffset,
40392
+ onChangeLayoutMode,
40393
+ onChangeShowIssues,
40394
+ onChangeShowCommits,
40395
+ onChangeAllBoards,
40396
+ 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)(
40401
+ "button",
40402
+ {
40403
+ title: "Earlier",
40404
+ onClick: () => onChangeOffset(offset + 1),
40405
+ style: navButtonStyle,
40406
+ children: "\u25C0"
40407
+ }
40408
+ ),
40409
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40410
+ "span",
40411
+ {
40412
+ style: {
40413
+ fontSize: 10,
40414
+ color: GUI_THEME.dim,
40415
+ whiteSpace: "nowrap",
40416
+ overflow: "hidden",
40417
+ // Fixed, not min, so the changing label never shifts the
40418
+ // buttons around it.
40419
+ width: 88,
40420
+ flexShrink: 0,
40421
+ textAlign: "center"
40422
+ },
40423
+ children: formatPeriodLabel(scope, offset, periodRange)
40424
+ }
40425
+ ),
40426
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40427
+ "button",
40428
+ {
40429
+ title: "Later",
40430
+ disabled: offset === 0,
40431
+ onClick: () => onChangeOffset(Math.max(0, offset - 1)),
40432
+ style: {
40433
+ ...navButtonStyle,
40434
+ opacity: offset === 0 ? 0.35 : 1,
40435
+ cursor: offset === 0 ? "default" : "pointer"
40436
+ },
40437
+ children: "\u25B6"
40438
+ }
40439
+ )
40440
+ ] }),
40441
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("div", { style: { display: "flex", gap: 2 }, children: SCOPES.map((option) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40442
+ "button",
40443
+ {
40444
+ onClick: () => onChangeScope(option),
40445
+ style: toggleButtonStyle(scope === option),
40446
+ children: scopeButtonLabel(option)
40447
+ },
40448
+ option
40449
+ )) })
40450
+ ] }),
40451
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { style: { display: "flex", gap: 2 }, children: [
40452
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40453
+ "button",
40454
+ {
40455
+ title: "How much happened, per equal-width period \u2014 no empty gaps for quiet stretches",
40456
+ onClick: () => onChangeLayoutMode("even"),
40457
+ style: toggleButtonStyle(layoutMode === "even"),
40458
+ children: "Volume"
40459
+ }
40460
+ ),
40461
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40462
+ "button",
40463
+ {
40464
+ title: "Individual events by exact moment \u2014 x is elapsed time, y is time of day",
40465
+ onClick: () => onChangeLayoutMode("real"),
40466
+ style: toggleButtonStyle(layoutMode === "real"),
40467
+ children: "Events"
40468
+ }
40469
+ )
40470
+ ] }),
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)(
40482
+ Checkbox,
40483
+ {
40484
+ label: "Code",
40485
+ checked: showCommits,
40486
+ activeColor: GUI_THEME.green,
40487
+ onChange: onChangeShowCommits
40488
+ }
40489
+ ),
40490
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40491
+ Checkbox,
40492
+ {
40493
+ label: "All boards",
40494
+ checked: allBoards,
40495
+ activeColor: GUI_THEME.accent,
40496
+ onChange: onChangeAllBoards
40497
+ }
40498
+ )
40499
+ ] }),
40500
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40501
+ "div",
40502
+ {
40503
+ style: {
40504
+ display: "flex",
40505
+ justifyContent: "flex-end",
40506
+ alignItems: "center",
40507
+ // Fixed, not min, so swapping between "Now" and "Return to live"
40508
+ // never resizes the controls row.
40509
+ width: 100,
40510
+ flexShrink: 0
40511
+ },
40512
+ children: isScrubbing ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40513
+ "button",
40514
+ {
40515
+ onClick: onReturnToLive,
40516
+ style: {
40517
+ background: "transparent",
40518
+ border: `1px solid ${GUI_THEME.accent}`,
40519
+ color: GUI_THEME.accent,
40520
+ borderRadius: 6,
40521
+ fontSize: 11,
40522
+ padding: "2px 8px",
40523
+ cursor: "pointer",
40524
+ whiteSpace: "nowrap"
40525
+ },
40526
+ children: "Return to live"
40527
+ }
40528
+ ) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40529
+ "span",
40530
+ {
40531
+ style: {
40532
+ fontSize: 11,
40533
+ color: GUI_THEME.dim,
40534
+ overflow: "hidden",
40535
+ textAlign: "right",
40536
+ whiteSpace: "nowrap"
40537
+ },
40538
+ children: nowLabel
40539
+ }
40540
+ )
40541
+ }
40542
+ )
40543
+ ] });
40544
+ var ScrubberHeader = ({
40545
+ collapsed,
40546
+ onToggleCollapsed,
40547
+ scrubbingAsOf
40548
+ }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
40549
+ "div",
40550
+ {
40551
+ style: {
40552
+ display: "flex",
40553
+ alignItems: "center",
40554
+ gap: 8,
40555
+ fontSize: 11,
40556
+ whiteSpace: "nowrap"
40557
+ },
40558
+ children: [
40559
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
40560
+ "button",
40561
+ {
40562
+ onClick: onToggleCollapsed,
40563
+ title: collapsed ? "Show time travel" : "Hide time travel",
40564
+ style: {
40565
+ background: "transparent",
40566
+ border: "none",
40567
+ color: GUI_THEME.dim,
40568
+ fontSize: 11,
40569
+ padding: 0,
40570
+ cursor: "pointer",
40571
+ display: "flex",
40572
+ alignItems: "center",
40573
+ gap: 4
40574
+ },
40575
+ children: [
40576
+ "Time travel",
40577
+ collapsed ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(IconChevronRight, { size: 12 }) : /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(IconChevronDown, { size: 12 })
40578
+ ]
40579
+ }
40580
+ ),
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 })
40584
+ ] })
40585
+ ]
40586
+ }
40587
+ );
40588
+ var SegmentHighlight = ({
40589
+ segment,
40590
+ fractionForTime
40591
+ }) => {
40592
+ const left = fractionForTime(segment.start);
40593
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40594
+ "div",
40595
+ {
40596
+ style: {
40597
+ position: "absolute",
40598
+ left: `${left * 100}%`,
40599
+ width: `${(fractionForTime(segment.end) - left) * 100}%`,
40600
+ top: 0,
40601
+ bottom: 0,
40602
+ background: SEGMENT_HIGHLIGHT_COLOR,
40603
+ pointerEvents: "none",
40604
+ display: "flex",
40605
+ justifyContent: "center",
40606
+ paddingTop: 3,
40607
+ // With top and bottom both pinned, content-box would add the padding
40608
+ // on top of the resolved height and push the block past the wrapper
40609
+ // it spans.
40610
+ boxSizing: "border-box",
40611
+ fontSize: 9,
40612
+ color: GUI_THEME.dim2,
40613
+ whiteSpace: "nowrap",
40614
+ // The label is wider than its block at month and year scopes; only
40615
+ // one segment is ever highlighted, so it has nothing to overlap.
40616
+ overflow: "visible"
40617
+ },
40618
+ children: segment.label
40619
+ }
40620
+ );
40621
+ };
40622
+ var TrackBaseline = ({
40623
+ color: color2,
40624
+ anchor
40625
+ }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40626
+ "div",
40627
+ {
40628
+ style: {
40629
+ position: "absolute",
40630
+ left: 0,
40631
+ right: 0,
40632
+ ...anchor === "bottom" ? { bottom: 0 } : {},
40633
+ ...anchor === "top" ? { top: 0 } : {},
40634
+ height: anchor === "top" ? 1 : 2,
40635
+ borderRadius: 999,
40636
+ background: color2,
40637
+ opacity: 0.2
40638
+ }
40639
+ }
40640
+ );
40641
+ var HourAxisLabels = () => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_jsx_runtime29.Fragment, { children: [
40642
+ ["00:00", { top: EVENTS_MODE_VERTICAL_PADDING }],
40643
+ ["12:00", { top: "50%", transform: "translateY(-50%)" }],
40644
+ ["24:00", { bottom: EVENTS_MODE_VERTICAL_PADDING }]
40645
+ ].map(([label, position3]) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40646
+ "span",
40647
+ {
40648
+ style: {
40649
+ position: "absolute",
40650
+ left: 2,
40651
+ fontSize: 9,
40652
+ color: GUI_THEME.dim,
40653
+ pointerEvents: "none",
40654
+ ...position3
40655
+ },
40656
+ children: label
40657
+ },
40658
+ label
40659
+ )) });
40660
+ var SeriesLayer = ({
40661
+ animate,
40662
+ children
40663
+ }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40664
+ "div",
40665
+ {
40666
+ style: {
40667
+ position: "absolute",
40668
+ inset: 0,
40669
+ pointerEvents: "none",
40670
+ animation: animate ? FADE_IN_ANIMATION : void 0
40671
+ },
40672
+ children
40673
+ }
40674
+ );
40675
+ var VolumeBars = ({
40676
+ bars,
40677
+ bucketCount,
40678
+ firstBar,
40679
+ lastBar,
40680
+ highlightedIndex,
40681
+ color: color2,
40682
+ direction,
40683
+ animate
40684
+ }) => {
40685
+ const widthPercent = 100 / bucketCount;
40686
+ const barWidth = barWidthCss(bucketCount);
40687
+ 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)(
40690
+ "div",
40691
+ {
40692
+ style: {
40693
+ position: "absolute",
40694
+ left: `${highlightedIndex * widthPercent}%`,
40695
+ top: 0,
40696
+ bottom: 0,
40697
+ width: `${widthPercent}%`,
40698
+ // A bucket can be thinner than a pixel at wide spans.
40699
+ minWidth: 2,
40700
+ background: BUCKET_HIGHLIGHT_COLOR,
40701
+ pointerEvents: "none"
40702
+ }
40703
+ }
40704
+ ),
40705
+ bars.map(({ index: index2, intensity }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40706
+ "div",
40707
+ {
40708
+ style: {
40709
+ position: "absolute",
40710
+ left: `${index2 * widthPercent}%`,
40711
+ ...growsUp ? { bottom: 0 } : { top: 0 },
40712
+ width: barWidth,
40713
+ height: 3 + intensity * (TRACK_HEIGHT - 3),
40714
+ borderRadius: growsUp ? "1px 1px 0 0" : "0 0 1px 1px",
40715
+ background: color2,
40716
+ opacity: 0.35 + intensity * 0.65,
40717
+ transformOrigin: growsUp ? "bottom" : "top",
40718
+ animation: animate ? barGrowAnimation(index2, firstBar, lastBar) : void 0,
40719
+ pointerEvents: "none"
40720
+ }
40721
+ },
40722
+ index2
40723
+ ))
40724
+ ] });
40725
+ };
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
+ var ScrubberNeedle = ({ fraction }) => {
40768
+ const [hovered, setHovered] = (0, import_react13.useState)(false);
40769
+ const hover = {
40770
+ onMouseEnter: () => setHovered(true),
40771
+ onMouseLeave: () => setHovered(false)
40772
+ };
40773
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
40774
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40775
+ "div",
40776
+ {
40777
+ ...hover,
40778
+ style: {
40779
+ position: "absolute",
40780
+ left: `${fraction * 100}%`,
40781
+ top: 0,
40782
+ bottom: 0,
40783
+ // A hairline, no glow: the needle marks an exact instant, and a
40784
+ // soft edge blooms over bars that can be ~2px wide.
40785
+ width: 1,
40786
+ background: NEEDLE_COLOR,
40787
+ zIndex: 3,
40788
+ transform: "translateX(-0.5px)",
40789
+ pointerEvents: "auto",
40790
+ cursor: "pointer"
40791
+ }
40792
+ }
40793
+ ),
40794
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40795
+ "div",
40796
+ {
40797
+ ...hover,
40798
+ style: {
40799
+ position: "absolute",
40800
+ left: `${fraction * 100}%`,
40801
+ top: hovered ? -9 : -7,
40802
+ width: 0,
40803
+ height: 0,
40804
+ borderLeft: `${hovered ? 6 : 5}px solid transparent`,
40805
+ borderRight: `${hovered ? 6 : 5}px solid transparent`,
40806
+ borderTop: `${hovered ? 8 : 7}px solid ${NEEDLE_COLOR}`,
40807
+ zIndex: 3,
40808
+ transform: `translateX(${hovered ? -6 : -5}px)`,
40809
+ pointerEvents: "auto",
40810
+ cursor: "pointer"
40811
+ }
40812
+ }
40813
+ )
40814
+ ] });
40815
+ };
40816
+ var ScrubberHoverHint = ({
40817
+ label,
40818
+ rows,
40819
+ segmentLabel,
40820
+ stripeColor,
40821
+ fraction,
40822
+ trackWidthPx,
40823
+ empty: empty3 = false
40824
+ }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
40825
+ "div",
40826
+ {
40827
+ style: {
40828
+ position: "absolute",
40829
+ top: "100%",
40830
+ marginTop: 6,
40831
+ left: clamp(
40832
+ fraction * trackWidthPx - HOVER_HINT_WIDTH / 2,
40833
+ 0,
40834
+ Math.max(0, trackWidthPx - HOVER_HINT_WIDTH)
40835
+ ),
40836
+ width: HOVER_HINT_WIDTH,
40837
+ // Border-box so `width` matches what the clamp math assumes; otherwise
40838
+ // border and padding push the box past the track's edge.
40839
+ boxSizing: "border-box",
40840
+ display: "flex",
40841
+ flexDirection: "column",
40842
+ gap: 2,
40843
+ textAlign: "left",
40844
+ background: GUI_THEME.panel,
40845
+ border: `1px solid ${GUI_THEME.line}`,
40846
+ borderLeft: `3px solid ${empty3 ? GUI_THEME.dim : stripeColor}`,
40847
+ // Smaller than the 6px used elsewhere: a rounder corner clips the 3px
40848
+ // stripe into a visible wedge.
40849
+ borderRadius: 3,
40850
+ padding: "6px 10px",
40851
+ pointerEvents: "none",
40852
+ // Above the board content this overhangs.
40853
+ zIndex: 5
40854
+ },
40855
+ 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)(
40858
+ "div",
40859
+ {
40860
+ style: {
40861
+ fontSize: 11,
40862
+ fontWeight: 600,
40863
+ color: empty3 ? GUI_THEME.secondary : GUI_THEME.primary,
40864
+ whiteSpace: "normal",
40865
+ wordBreak: "break-word"
40866
+ },
40867
+ children: label
40868
+ }
40869
+ ),
40870
+ rows.map((row, index2) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40871
+ "div",
40872
+ {
40873
+ style: {
40874
+ fontSize: 11,
40875
+ color: empty3 ? GUI_THEME.dim : GUI_THEME.secondary,
40876
+ whiteSpace: "normal",
40877
+ wordBreak: "break-word"
40878
+ },
40879
+ children: row
40880
+ },
40881
+ index2
40882
+ ))
40883
+ ]
40884
+ }
40885
+ );
40886
+
40887
+ // 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);
40890
+ var ScrubberLayout = ({
40891
+ collapsed,
40892
+ onToggleCollapsed,
40893
+ scrubbingAsOf,
40894
+ controls,
40895
+ chart
40896
+ }) => {
40897
+ const { axis, layoutMode, animate, windowKey, on } = chart;
40898
+ return /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40899
+ Panel,
40900
+ {
40901
+ as: "div",
40902
+ borderColor: GUI_THEME.line,
40903
+ borderRadius: 0,
40904
+ style: {
40905
+ borderLeft: "none",
40906
+ borderRight: "none",
40907
+ borderTop: "none",
40908
+ padding: "10px 30px",
40909
+ // Panel clips children to keep its glow inside its rounded corners,
40910
+ // which would lop off the overhanging hint. Safe to disable only
40911
+ // because this panel is square.
40912
+ overflow: "visible"
40913
+ },
40914
+ children: [
40915
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("style", { children: SCRUBBER_KEYFRAMES }),
40916
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40917
+ "div",
40918
+ {
40919
+ style: {
40920
+ display: "flex",
40921
+ flexDirection: "column",
40922
+ gap: collapsed ? 0 : 8
40923
+ },
40924
+ children: [
40925
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40926
+ "div",
40927
+ {
40928
+ style: {
40929
+ display: "flex",
40930
+ alignItems: "center",
40931
+ justifyContent: "space-between",
40932
+ // Fixed whether or not the banner renders, so the track below
40933
+ // never shifts vertically.
40934
+ minHeight: 22
40935
+ },
40936
+ children: [
40937
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40938
+ ScrubberHeader,
40939
+ {
40940
+ collapsed,
40941
+ onToggleCollapsed,
40942
+ scrubbingAsOf
40943
+ }
40944
+ ),
40945
+ !collapsed && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ScrubberControls, { ...controls })
40946
+ ]
40947
+ }
40948
+ ),
40949
+ !collapsed && // Wraps both charts so the period highlight can be one tall block
40950
+ // spanning them and the gap between. Pointer handlers belong here
40951
+ // rather than on either chart, so a drag or hover anywhere across
40952
+ // the pair — the gap included — counts as one timeline.
40953
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40954
+ "div",
40955
+ {
40956
+ onPointerDown: on.onPointerDown,
40957
+ onPointerMove: on.onPointerMove,
40958
+ onPointerUp: on.onPointerEnd,
40959
+ onPointerCancel: on.onPointerEnd,
40960
+ onMouseMove: on.onTrackMouseMove,
40961
+ onMouseLeave: on.onTrackMouseLeave,
40962
+ style: {
40963
+ position: "relative",
40964
+ display: "flex",
40965
+ flexDirection: "column",
40966
+ gap: 8,
40967
+ cursor: "pointer"
40968
+ },
40969
+ children: [
40970
+ chart.hoveredSegment && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40971
+ SegmentHighlight,
40972
+ {
40973
+ segment: chart.hoveredSegment,
40974
+ fractionForTime: axis.fractionForTime
40975
+ }
40976
+ ),
40977
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
40978
+ "div",
40979
+ {
40980
+ ref: chart.trackRef,
40981
+ style: {
40982
+ position: "relative",
40983
+ width: "100%",
40984
+ height: layoutMode === "real" ? EVENTS_SCATTER_HEIGHT : TRACK_HEIGHT,
40985
+ paddingTop: layoutMode === "real" ? EVENTS_MODE_VERTICAL_PADDING : 0,
40986
+ paddingBottom: layoutMode === "real" ? EVENTS_MODE_VERTICAL_PADDING : 0,
40987
+ boxSizing: "content-box",
40988
+ display: "flex",
40989
+ alignItems: "center"
40990
+ },
40991
+ children: [
40992
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
40993
+ TrackBaseline,
40994
+ {
40995
+ color: GUI_THEME.accent,
40996
+ anchor: layoutMode === "even" ? "bottom" : "centre"
40997
+ }
40998
+ ),
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)(
41001
+ VolumeBars,
41002
+ {
41003
+ bars: chart.issueBars,
41004
+ bucketCount: axis.bucketCount,
41005
+ firstBar: chart.issueBarRange[0],
41006
+ lastBar: chart.issueBarRange[1],
41007
+ highlightedIndex: chart.hoveredBucketIndex,
41008
+ color: GUI_THEME.accent,
41009
+ direction: "up",
41010
+ animate
41011
+ }
41012
+ ) }, `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,
41046
+ {
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}`
41072
+ )
41073
+ ]
41074
+ }
41075
+ ),
41076
+ chart.showCommits && layoutMode === "even" && chart.commits.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime30.jsxs)(
41077
+ "div",
41078
+ {
41079
+ onMouseEnter: on.onCommitTrackMouseEnter,
41080
+ onMouseMove: on.onCommitTrackMouseMove,
41081
+ onMouseLeave: on.onCommitTrackMouseLeave,
41082
+ style: {
41083
+ position: "relative",
41084
+ width: "100%",
41085
+ height: TRACK_HEIGHT,
41086
+ animation: animate ? FADE_IN_ANIMATION : void 0
41087
+ },
41088
+ children: [
41089
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(TrackBaseline, { color: GUI_THEME.green, anchor: "top" }),
41090
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41091
+ VolumeBars,
41092
+ {
41093
+ bars: chart.commitBars,
41094
+ bucketCount: axis.bucketCount,
41095
+ firstBar: chart.commitBarRange[0],
41096
+ lastBar: chart.commitBarRange[1],
41097
+ highlightedIndex: chart.hoveredCommitBucketIndex,
41098
+ color: GUI_THEME.green,
41099
+ direction: "down",
41100
+ animate
41101
+ }
41102
+ )
41103
+ ]
41104
+ },
41105
+ `commits-${windowKey}`
41106
+ ),
41107
+ /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(ScrubberNeedle, { fraction: chart.thumbFraction }),
41108
+ chart.boardHint && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41109
+ ScrubberHoverHint,
41110
+ {
41111
+ ...chart.boardHint,
41112
+ segmentLabel: chart.hoveredSegment?.label,
41113
+ stripeColor: GUI_THEME.accent,
41114
+ trackWidthPx: chart.trackWidthPx
41115
+ }
41116
+ ),
41117
+ chart.commitHint && /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41118
+ ScrubberHoverHint,
41119
+ {
41120
+ ...chart.commitHint,
41121
+ segmentLabel: chart.hoveredSegment?.label,
41122
+ stripeColor: GUI_THEME.green,
41123
+ trackWidthPx: chart.trackWidthPx
41124
+ }
41125
+ )
41126
+ ]
41127
+ }
41128
+ )
41129
+ ]
41130
+ }
41131
+ )
39576
41132
  ]
39577
41133
  }
39578
41134
  );
39579
41135
  };
39580
41136
 
41137
+ // source/gui/client/components/TimeScrubber.tsx
41138
+ var import_jsx_runtime31 = __toESM(require_jsx_runtime(), 1);
41139
+ var SCRUB_THROTTLE_MS = 120;
41140
+ var COLLAPSED_STORAGE_KEY = "epiq.timeScrubber.collapsed";
41141
+ var SCOPE_STORAGE_KEY = "epiq.timeScrubber.scope";
41142
+ var SHOW_ISSUES_STORAGE_KEY = "epiq.timeScrubber.showIssues";
41143
+ var SHOW_COMMITS_STORAGE_KEY = "epiq.timeScrubber.showCommits";
41144
+ var ALL_BOARDS_STORAGE_KEY = "epiq.timeScrubber.allBoards";
41145
+ var readStoredScope = () => {
41146
+ const stored = localStorage.getItem(SCOPE_STORAGE_KEY);
41147
+ return isScope(stored) ? stored : "all";
41148
+ };
41149
+ var TimeScrubber = ({
41150
+ timeline,
41151
+ commits,
41152
+ timeTravel,
41153
+ onScrub,
41154
+ onReturnToLive,
41155
+ onRequestHistory,
41156
+ boardId,
41157
+ connected,
41158
+ onInspectCommit
41159
+ }) => {
41160
+ 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);
41167
+ const [collapsed, setCollapsed] = usePersistedFlag(
41168
+ COLLAPSED_STORAGE_KEY,
41169
+ false
41170
+ );
41171
+ const [showIssues, setShowIssues] = usePersistedFlag(
41172
+ SHOW_ISSUES_STORAGE_KEY,
41173
+ true
41174
+ );
41175
+ const [showCommits, setShowCommits] = usePersistedFlag(
41176
+ SHOW_COMMITS_STORAGE_KEY,
41177
+ true
41178
+ );
41179
+ const [allBoards, setAllBoards] = usePersistedFlag(
41180
+ ALL_BOARDS_STORAGE_KEY,
41181
+ false
41182
+ );
41183
+ const exitMs = layoutMode === "real" && animate ? DOT_EXIT_TOTAL_MS : 0;
41184
+ const issueScatter = useExitTransition(showIssues, exitMs);
41185
+ 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)(
41190
+ null
41191
+ );
41192
+ const [hoveredCommitBucketIndex, setHoveredCommitBucketIndex] = (0, import_react14.useState)(null);
41193
+ const periodRange = getPeriodRange(scope, offset);
41194
+ (0, import_react14.useEffect)(() => {
41195
+ if (!connected) return;
41196
+ onRequestHistory(periodRange?.start, periodRange?.end, allBoards);
41197
+ }, [scope, offset, boardId, allBoards, connected]);
41198
+ const changeScope = (nextScope) => {
41199
+ setScope(nextScope);
41200
+ setOffset(0);
41201
+ localStorage.setItem(SCOPE_STORAGE_KEY, nextScope);
41202
+ };
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
41211
+ );
41212
+ const maxCommitCount = maxOf(
41213
+ Array.from(commitStats.values(), (stats) => stats.count),
41214
+ 1
41215
+ );
41216
+ const issueBars = issueCounts.flatMap(
41217
+ (count, index2) => count > 0 ? [{ index: index2, intensity: count / maxIssueBucketCount }] : []
41218
+ );
41219
+ const commitBars = Array.from(commitStats, ([index2, stats]) => ({
41220
+ index: index2,
41221
+ intensity: stats.count / maxCommitCount
41222
+ }));
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]);
41228
+ const confirmedFraction = timeTravel.mode === "scrub" && timeTravel.asOfTime !== null ? axis.fractionForTime(timeTravel.asOfTime) : 1;
41229
+ const fractionFromClientX = (clientX) => {
41230
+ const track = trackRef.current;
41231
+ if (!track) return 0;
41232
+ const rect = track.getBoundingClientRect();
41233
+ return clamp((clientX - rect.left) / Math.max(1, rect.width), 0, 1);
41234
+ };
41235
+ const dispatchScrub = (fraction, force) => {
41236
+ const now = Date.now();
41237
+ if (!force && now - lastDispatchRef.current < SCRUB_THROTTLE_MS) return;
41238
+ lastDispatchRef.current = now;
41239
+ onScrub(axis.fractionToTime(fraction));
41240
+ };
41241
+ const endDrag = () => {
41242
+ if (dragFraction === null) return;
41243
+ dispatchScrub(dragFraction, true);
41244
+ setDragFraction(null);
41245
+ };
41246
+ const fractionFromEvent = (event) => {
41247
+ const rect = event.currentTarget.getBoundingClientRect();
41248
+ return clamp((event.clientX - rect.left) / Math.max(1, rect.width), 0, 1);
41249
+ };
41250
+ const bucketIndexFromEvent = (event) => clamp(
41251
+ Math.floor(fractionFromEvent(event) * axis.bucketCount),
41252
+ 0,
41253
+ axis.bucketCount - 1
41254
+ );
41255
+ const bucketTimeAt = (index2) => index2 !== null && index2 < axis.bucketCount ? axis.bucketTimeAt(index2) : void 0;
41256
+ const centreFraction = (index2) => (index2 + 0.5) / axis.bucketCount;
41257
+ const boardEventRow = (count) => `${count} board event${count === 1 ? "" : "s"}`;
41258
+ const hoveredBucketTime = bucketTimeAt(hoveredBucketIndex);
41259
+ const hoveredBucketCount = hoveredBucketIndex !== null ? issueCounts[hoveredBucketIndex] : void 0;
41260
+ const boardHint = layoutMode === "even" ? hoveredBucketTime !== void 0 && hoveredBucketCount !== void 0 ? {
41261
+ label: formatInterval(
41262
+ hoveredBucketTime,
41263
+ hoveredBucketTime + axis.bucketMs
41264
+ ),
41265
+ rows: [boardEventRow(hoveredBucketCount)],
41266
+ fraction: centreFraction(hoveredBucketIndex),
41267
+ empty: hoveredBucketCount === 0
41268
+ } : null : hoveredEvent ? {
41269
+ label: hoveredEvent.label,
41270
+ rows: [boardEventRow(hoveredEvent.count)],
41271
+ fraction: hoveredEvent.fraction
41272
+ } : null;
41273
+ const hoveredCommitStats = hoveredCommitBucketIndex !== null ? commitStats.get(hoveredCommitBucketIndex) : void 0;
41274
+ const hoveredCommitBucketTime = bucketTimeAt(hoveredCommitBucketIndex);
41275
+ const commitHint = layoutMode === "even" ? hoveredCommitStats && hoveredCommitBucketTime !== void 0 ? {
41276
+ label: formatInterval(
41277
+ hoveredCommitBucketTime,
41278
+ hoveredCommitBucketTime + axis.bucketMs
41279
+ ),
41280
+ rows: [
41281
+ `${hoveredCommitStats.count} commit${hoveredCommitStats.count === 1 ? "" : "s"}`,
41282
+ `${hoveredCommitStats.linesChanged.toLocaleString()} lines changed`
41283
+ ],
41284
+ fraction: centreFraction(hoveredCommitBucketIndex)
41285
+ } : null : hoveredCommit ? {
41286
+ label: formatDateTime(new Date(hoveredCommit.commit.time)),
41287
+ rows: [
41288
+ hoveredCommit.commit.subject,
41289
+ `${hoveredCommit.commit.author} \u2022 ${hoveredCommit.commit.linesChanged.toLocaleString()} lines`
41290
+ ],
41291
+ fraction: hoveredCommit.fraction
41292
+ } : null;
41293
+ const hoveredSegmentTime = (layoutMode === "even" ? hoveredBucketTime ?? bucketTimeAt(hoveredCommitBucketIndex) : (
41294
+ // A hovered point wins over the raw pointer, so the highlight agrees
41295
+ // with the tooltip's own moment.
41296
+ hoveredEvent?.t ?? hoveredCommit?.commit.time ?? (pointerFraction !== null ? axis.fractionToTime(pointerFraction) : void 0)
41297
+ )) ?? null;
41298
+ const segmentUnit = chooseSegmentUnit(axis.span);
41299
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
41300
+ ScrubberLayout,
41301
+ {
41302
+ collapsed,
41303
+ onToggleCollapsed: () => setCollapsed(!collapsed),
41304
+ scrubbingAsOf: timeTravel.mode === "scrub" ? timeTravel.asOfTime ? formatDateTime(new Date(timeTravel.asOfTime)) : "" : null,
41305
+ controls: {
41306
+ scope,
41307
+ offset,
41308
+ periodRange,
41309
+ layoutMode,
41310
+ showIssues,
41311
+ showCommits,
41312
+ allBoards,
41313
+ isScrubbing: timeTravel.mode === "scrub",
41314
+ nowLabel: scope === "all" || offset === 0 ? "Now" : formatDateTime(new Date(axis.latest)),
41315
+ onChangeScope: changeScope,
41316
+ onChangeOffset: setOffset,
41317
+ onChangeLayoutMode: setLayoutMode,
41318
+ onChangeShowIssues: setShowIssues,
41319
+ onChangeShowCommits: setShowCommits,
41320
+ onChangeAllBoards: setAllBoards,
41321
+ onReturnToLive
41322
+ },
41323
+ chart: {
41324
+ trackRef,
41325
+ axis,
41326
+ layoutMode,
41327
+ animate,
41328
+ windowKey: `${layoutMode}-${animationGeneration}`,
41329
+ showIssues,
41330
+ showCommits,
41331
+ issueScatter,
41332
+ commitScatter,
41333
+ issueBars,
41334
+ issueBarRange: populatedRange(issueBars),
41335
+ commitBars,
41336
+ commitBarRange: populatedRange(commitBars),
41337
+ eventBuckets,
41338
+ maxEventCount,
41339
+ commits,
41340
+ hoveredCommitSha: hoveredCommit?.commit.sha ?? null,
41341
+ hoveredBucketIndex,
41342
+ hoveredCommitBucketIndex,
41343
+ hoveredSegment: hoveredSegmentTime !== null ? segmentAt(hoveredSegmentTime, segmentUnit) : null,
41344
+ thumbFraction: dragFraction ?? confirmedFraction,
41345
+ trackWidthPx: trackRef.current?.clientWidth ?? 0,
41346
+ boardHint,
41347
+ commitHint,
41348
+ on: {
41349
+ onPointerDown: (event) => {
41350
+ event.currentTarget.setPointerCapture(event.pointerId);
41351
+ const fraction = fractionFromClientX(event.clientX);
41352
+ setDragFraction(fraction);
41353
+ dispatchScrub(fraction, true);
41354
+ },
41355
+ onPointerMove: (event) => {
41356
+ if (dragFraction === null) return;
41357
+ const fraction = fractionFromClientX(event.clientX);
41358
+ setDragFraction(fraction);
41359
+ dispatchScrub(fraction, false);
41360
+ },
41361
+ onPointerEnd: endDrag,
41362
+ onTrackMouseMove: (event) => {
41363
+ if (layoutMode === "even") {
41364
+ setHoveredBucketIndex(bucketIndexFromEvent(event));
41365
+ return;
41366
+ }
41367
+ setPointerFraction(fractionFromEvent(event));
41368
+ },
41369
+ onTrackMouseLeave: () => {
41370
+ setHoveredBucketIndex(null);
41371
+ setPointerFraction(null);
41372
+ },
41373
+ onCommitTrackMouseEnter: () => setHoveredBucketIndex(null),
41374
+ onCommitTrackMouseMove: (event) => {
41375
+ event.stopPropagation();
41376
+ setHoveredCommitBucketIndex(bucketIndexFromEvent(event));
41377
+ },
41378
+ 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),
41391
+ onInspectCommit
41392
+ }
41393
+ }
41394
+ }
41395
+ );
41396
+ };
41397
+
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
+
39581
41426
  // source/gui/client/lib/gui-move-issue.tsx
39582
41427
  var toMovePosition = (targetIssues, targetIndex) => {
39583
41428
  if (targetIssues.length === 0) return { at: "start" };
@@ -39655,16 +41500,14 @@ var moveIssue = (setState, socketRef) => (issueId, parentId, targetIndex) => {
39655
41500
  };
39656
41501
  });
39657
41502
  if (!position3) return;
39658
- socketRef.current?.send(
39659
- JSON.stringify({
39660
- type: "issues:move",
39661
- payload: {
39662
- issueId,
39663
- parentId,
39664
- position: position3
39665
- }
39666
- })
39667
- );
41503
+ sendSocketJson(socketRef.current, {
41504
+ type: "issues:move",
41505
+ payload: {
41506
+ issueId,
41507
+ parentId,
41508
+ position: position3
41509
+ }
41510
+ });
39668
41511
  };
39669
41512
 
39670
41513
  // source/lib/utils/node-ref.ts
@@ -39709,6 +41552,30 @@ var updateIssueInGuiState = (state, issueId, updateIssue) => ({
39709
41552
  }))
39710
41553
  });
39711
41554
 
41555
+ // source/gui/client/lib/history-buffer.ts
41556
+ var createHistoryBuffer = (publish) => {
41557
+ let lastRequestId = 0;
41558
+ let pending2 = {};
41559
+ return {
41560
+ /** Starts a new window, abandoning whatever the previous one buffered. */
41561
+ open() {
41562
+ lastRequestId += 1;
41563
+ pending2 = { requestId: lastRequestId };
41564
+ return lastRequestId;
41565
+ },
41566
+ /** Files one half, ignoring replies to any request but the open one. */
41567
+ accept(requestId, half) {
41568
+ if (requestId === void 0) return;
41569
+ if (requestId !== pending2.requestId) return;
41570
+ pending2 = { ...pending2, ...half };
41571
+ const { timeline, commits } = pending2;
41572
+ if (!timeline || !commits) return;
41573
+ pending2 = {};
41574
+ publish({ timeline, commits });
41575
+ }
41576
+ };
41577
+ };
41578
+
39712
41579
  // source/gui/client/lib/compress-image.ts
39713
41580
  var MAX_LONG_EDGE = 1600;
39714
41581
  var DEFAULT_MAX_KB = 500;
@@ -39774,8 +41641,8 @@ var blobToBase64 = (blob) => new Promise((resolve, reject) => {
39774
41641
  });
39775
41642
 
39776
41643
  // source/gui/client/App.tsx
39777
- var import_jsx_runtime21 = __toESM(require_jsx_runtime(), 1);
39778
- var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
41644
+ var import_jsx_runtime32 = __toESM(require_jsx_runtime(), 1);
41645
+ var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
39779
41646
  "div",
39780
41647
  {
39781
41648
  style: {
@@ -39790,26 +41657,36 @@ var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
39790
41657
  var App = () => {
39791
41658
  const { boardId, issueId } = useParams();
39792
41659
  const [searchParams, setSearchParams] = useSearchParams();
39793
- const [connected, setConnected] = (0, import_react11.useState)(false);
39794
- const [syncStatus, setSyncStatus] = (0, import_react11.useState)({
41660
+ const [connected, setConnected] = (0, import_react15.useState)(false);
41661
+ const [syncStatus, setSyncStatus] = (0, import_react15.useState)({
39795
41662
  status: "synced",
39796
41663
  msg: "idle"
39797
41664
  });
39798
- const [state, setState] = (0, import_react11.useState)(null);
39799
- const [dragOverSwimlaneId, setDragOverSwimlaneId] = (0, import_react11.useState)(
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)(
41670
+ null
41671
+ );
41672
+ const [removeError, setRemoveError] = (0, import_react15.useState)(null);
41673
+ const [dragOverSwimlaneId, setDragOverSwimlaneId] = (0, import_react15.useState)(
39800
41674
  null
39801
41675
  );
39802
- const [dropTarget, setDropTarget] = (0, import_react11.useState)(null);
39803
- const [boardMenuOpen, setBoardMenuOpen] = (0, import_react11.useState)(false);
39804
- const [createIssueModal, setCreateIssueModal] = (0, import_react11.useState)(null);
39805
- const boardMenuRef = (0, import_react11.useRef)(null);
39806
- const socketRef = (0, import_react11.useRef)(null);
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);
39807
41681
  const selectedTab = searchParams.get("tab") === "comments" ? "comments" : "overview";
39808
41682
  const navigate = useNavigate();
39809
41683
  const selectedBoard = (state && boardId ? findBoard(state, boardId) : null) ?? state?.boards[0] ?? null;
41684
+ const selectedBoardId = selectedBoard?.id ?? null;
41685
+ const selectedBoardIdRef = (0, import_react15.useRef)(selectedBoardId);
41686
+ selectedBoardIdRef.current = selectedBoardId;
39810
41687
  const boardSlug = selectedBoard?.ref ?? boardId;
39811
41688
  const selectedIssue = state && issueId ? findIssue(state, issueId) : null;
39812
- (0, import_react11.useEffect)(() => {
41689
+ (0, import_react15.useEffect)(() => {
39813
41690
  const onPaste = (event) => {
39814
41691
  if (!selectedIssue || selectedIssue.readonly) return;
39815
41692
  const target = event.target;
@@ -39830,18 +41707,18 @@ var App = () => {
39830
41707
  };
39831
41708
  const commentsByIssueId = state?.commentsByIssueId ?? {};
39832
41709
  const attachmentsByIssueId = state?.attachmentsByIssueId ?? {};
39833
- const [attachmentUploadStatus, setAttachmentUploadStatus] = (0, import_react11.useState)({ state: "idle" });
41710
+ const [attachmentUploadStatus, setAttachmentUploadStatus] = (0, import_react15.useState)({ state: "idle" });
39834
41711
  const requestState = () => {
39835
- socketRef.current?.send(JSON.stringify({ type: "state:get" }));
41712
+ sendSocketJson(socketRef.current, { type: "state:get" });
39836
41713
  };
39837
- (0, import_react11.useEffect)(() => {
41714
+ (0, import_react15.useEffect)(() => {
39838
41715
  const socket = new WebSocket(
39839
41716
  `ws://${window.location.host}/ws${boardId ? `?boardId=${boardId}` : ""}`
39840
41717
  );
39841
41718
  socketRef.current = socket;
39842
41719
  socket.addEventListener("open", () => {
39843
41720
  setConnected(true);
39844
- socket.send(JSON.stringify({ type: "state:get" }));
41721
+ sendSocketJson(socket, { type: "state:get" });
39845
41722
  });
39846
41723
  socket.addEventListener("close", () => {
39847
41724
  setConnected(false);
@@ -39867,6 +41744,40 @@ var App = () => {
39867
41744
  console.log("Failed", message);
39868
41745
  requestState();
39869
41746
  }
41747
+ if (message.type === "timeline") {
41748
+ const nextTimeline = getResultValue(message.payload);
41749
+ if (nextTimeline) {
41750
+ historyBuffer.accept(message.requestId, { timeline: nextTimeline });
41751
+ }
41752
+ }
41753
+ if (message.type === "contributor:remove:result" || message.type === "issue:assignee:add:result") {
41754
+ sendSocketJson(socketRef.current, {
41755
+ type: "contributors:get",
41756
+ payload: { boardId: selectedBoardIdRef.current }
41757
+ });
41758
+ }
41759
+ if (message.type === "contributor:remove:result" && message.payload?.status === "fail") {
41760
+ setRemoveError(
41761
+ `Couldn't remove a contributor: ${message.payload.message}`
41762
+ );
41763
+ }
41764
+ if (message.type === "contributors") {
41765
+ const next = getResultValue(message.payload);
41766
+ if (next) setContributors(next);
41767
+ }
41768
+ if (message.type === "commits") {
41769
+ const nextCommits = getResultValue(message.payload);
41770
+ if (nextCommits) {
41771
+ historyBuffer.accept(message.requestId, { commits: nextCommits });
41772
+ }
41773
+ }
41774
+ if (message.type === "commit:inspect:result" && message.payload?.status === "fail") {
41775
+ setCommitInspectError(message.payload.message);
41776
+ }
41777
+ if (message.type === "time-travel:result" && message.payload?.status === "fail") {
41778
+ console.log("Time travel failed", message);
41779
+ requestState();
41780
+ }
39870
41781
  if (message.type === "sync-status") {
39871
41782
  setSyncStatus(message.payload);
39872
41783
  }
@@ -39878,12 +41789,12 @@ var App = () => {
39878
41789
  socket.close();
39879
41790
  };
39880
41791
  }, [boardId, navigate]);
39881
- (0, import_react11.useEffect)(() => {
41792
+ (0, import_react15.useEffect)(() => {
39882
41793
  if (!boardId && state?.boards[0]) {
39883
41794
  void navigate(`/board/${state.boards[0].ref}`, { replace: true });
39884
41795
  }
39885
41796
  }, [boardId, state, navigate]);
39886
- (0, import_react11.useEffect)(() => {
41797
+ (0, import_react15.useEffect)(() => {
39887
41798
  const close = (event) => {
39888
41799
  if (boardMenuRef.current && !boardMenuRef.current.contains(event.target)) {
39889
41800
  setBoardMenuOpen(false);
@@ -39897,7 +41808,7 @@ var App = () => {
39897
41808
  setDropTarget(null);
39898
41809
  };
39899
41810
  const send = (type, payload) => {
39900
- socketRef.current?.send(JSON.stringify({ type, payload }));
41811
+ sendSocketJson(socketRef.current, { type, payload });
39901
41812
  };
39902
41813
  const selectIssue = (nextIssueId) => {
39903
41814
  if (!boardSlug) return;
@@ -39971,7 +41882,22 @@ var App = () => {
39971
41882
  });
39972
41883
  send("issue:tag:remove", { issueId: issueId2, tagId });
39973
41884
  };
39974
- const addIssueAssignee = (issueId2, assigneeName) => {
41885
+ const addIssueAssignee = (issueId2, assigneeId) => {
41886
+ const picked = contributors.find((c) => c.id === assigneeId);
41887
+ setState((prev) => {
41888
+ if (!prev || !picked) return prev;
41889
+ return updateIssueInGuiState(
41890
+ prev,
41891
+ issueId2,
41892
+ (issue) => issue.assignees.some((assignee) => assignee.id === assigneeId) ? issue : { ...issue, assignees: [...issue.assignees, picked] }
41893
+ );
41894
+ });
41895
+ send("issue:assignee:add", { issueId: issueId2, assigneeId });
41896
+ };
41897
+ const removeContributor = (contributorId) => {
41898
+ send("contributor:remove", { contributorId });
41899
+ };
41900
+ const addExternalIssueAssignee = (issueId2, assigneeName) => {
39975
41901
  setState((prev) => {
39976
41902
  if (!prev) return prev;
39977
41903
  return updateIssueInGuiState(prev, issueId2, (issue) => {
@@ -39991,7 +41917,7 @@ var App = () => {
39991
41917
  };
39992
41918
  });
39993
41919
  });
39994
- send("issue:assignee:add", { issueId: issueId2, assigneeName });
41920
+ send("issue:assignee:add", { issueId: issueId2, assigneeName, createUnlinked: true });
39995
41921
  };
39996
41922
  const removeIssueAssignee = (issueId2, assigneeId) => {
39997
41923
  setState((prev) => {
@@ -40025,6 +41951,53 @@ var App = () => {
40025
41951
  });
40026
41952
  send("issue:reopen", { issueId: issueId2 });
40027
41953
  };
41954
+ const scrubToTime = (targetTime) => {
41955
+ send("time-travel:scrub", { targetTime });
41956
+ };
41957
+ const returnToLive = () => {
41958
+ send("time-travel:live", {});
41959
+ };
41960
+ const requestBoardHistory = (0, import_react15.useCallback)(
41961
+ (start2, end, allBoards) => {
41962
+ const window2 = start2 !== void 0 ? { start: start2, end } : void 0;
41963
+ const requestId = historyBuffer.open();
41964
+ sendSocketJson(socketRef.current, {
41965
+ type: "timeline:get",
41966
+ payload: {
41967
+ ...window2,
41968
+ boardId: allBoards ? void 0 : selectedBoardId,
41969
+ requestId
41970
+ }
41971
+ });
41972
+ sendSocketJson(socketRef.current, {
41973
+ type: "commits:get",
41974
+ payload: { ...window2, requestId }
41975
+ });
41976
+ },
41977
+ [selectedBoardId]
41978
+ );
41979
+ const requestContributors = (0, import_react15.useCallback)(() => {
41980
+ sendSocketJson(socketRef.current, {
41981
+ type: "contributors:get",
41982
+ payload: { boardId: selectedBoardIdRef.current }
41983
+ });
41984
+ }, []);
41985
+ (0, import_react15.useEffect)(() => {
41986
+ setContributors([]);
41987
+ }, [selectedBoardId]);
41988
+ const inspectCommit = (0, import_react15.useCallback)((sha) => {
41989
+ sendSocketJson(socketRef.current, { type: "commit:inspect", payload: { sha } });
41990
+ }, []);
41991
+ (0, import_react15.useEffect)(() => {
41992
+ if (!commitInspectError) return;
41993
+ const timeout = setTimeout(() => setCommitInspectError(null), 8e3);
41994
+ return () => clearTimeout(timeout);
41995
+ }, [commitInspectError]);
41996
+ (0, import_react15.useEffect)(() => {
41997
+ if (!removeError) return;
41998
+ const timeout = setTimeout(() => setRemoveError(null), 8e3);
41999
+ return () => clearTimeout(timeout);
42000
+ }, [removeError]);
40028
42001
  const selectBoard = (nextBoardId) => {
40029
42002
  setBoardMenuOpen(false);
40030
42003
  clearDragState();
@@ -40147,7 +42120,7 @@ var App = () => {
40147
42120
  });
40148
42121
  send("issue:comment:delete", { commentId });
40149
42122
  };
40150
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
42123
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
40151
42124
  "div",
40152
42125
  {
40153
42126
  style: {
@@ -40159,8 +42132,37 @@ var App = () => {
40159
42132
  flexDirection: "column"
40160
42133
  },
40161
42134
  children: [
40162
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Header, { state, connected, syncStatus }),
40163
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
42135
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(GlobalScrollbarStyles, {}),
42136
+ commitInspectError && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42137
+ ErrorToast,
42138
+ {
42139
+ message: `Couldn't open commit diff: ${commitInspectError}`,
42140
+ onDismiss: () => setCommitInspectError(null)
42141
+ }
42142
+ ),
42143
+ removeError && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42144
+ ErrorToast,
42145
+ {
42146
+ message: removeError,
42147
+ onDismiss: () => setRemoveError(null)
42148
+ }
42149
+ ),
42150
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Header, { state, connected, syncStatus }),
42151
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42152
+ TimeScrubber,
42153
+ {
42154
+ timeline: history.timeline,
42155
+ commits: history.commits,
42156
+ boardId: selectedBoardId,
42157
+ connected,
42158
+ onRequestHistory: requestBoardHistory,
42159
+ onInspectCommit: inspectCommit,
42160
+ timeTravel: state?.timeTravel ?? { mode: "live", asOfTime: null },
42161
+ onScrub: scrubToTime,
42162
+ onReturnToLive: returnToLive
42163
+ }
42164
+ ),
42165
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
40164
42166
  "div",
40165
42167
  {
40166
42168
  style: {
@@ -40169,47 +42171,76 @@ var App = () => {
40169
42171
  overflow: "hidden"
40170
42172
  },
40171
42173
  children: [
40172
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("main", { style: { padding: "0 30px 30px 30px", overflow: "auto", flex: 1 }, children: [
40173
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { style: { padding: "20px 10px" }, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
40174
- Dropdown,
40175
- {
40176
- label: "Board:",
40177
- value: selectedBoard ? {
40178
- id: selectedBoard.id,
40179
- label: selectedBoard.title
40180
- } : null,
40181
- items: state?.boards.map((board) => ({
40182
- id: board.id,
40183
- label: board.title
40184
- })) ?? [],
40185
- placeholder: "Loading...",
40186
- onSelect: selectBoard
40187
- }
40188
- ) }),
40189
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { style: { display: "flex", gap: 8 }, children: [
40190
- selectedBoard?.swimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
40191
- SwimlaneColumn,
40192
- {
40193
- swimlane,
40194
- selected: false,
40195
- selectedIssueId: selectedIssue?.id ?? null,
40196
- commentsByIssueId,
40197
- dragOver: dragOverSwimlaneId === swimlane.id,
40198
- dropIndex: dropTarget?.swimlaneId === swimlane.id ? dropTarget.index : null,
40199
- onSelectIssue: selectIssue,
40200
- onSelectIssueComments: selectIssueComments,
40201
- onCreateIssue: openCreateIssueModal,
40202
- onDropIssue: moveIssue(setState, socketRef),
40203
- onDragOver: setDragOverSwimlaneId,
40204
- onDragOverIssue: (swimlaneId, index2) => setDropTarget({ swimlaneId, index: index2 }),
40205
- onDragLeave: clearDragState
40206
- },
40207
- swimlane.id
40208
- )),
40209
- !(selectedIssue && state?.user) && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { style: { width: ASIDE_WIDTH, flexShrink: 0 } })
40210
- ] })
40211
- ] }),
40212
- selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
42174
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
42175
+ "main",
42176
+ {
42177
+ style: {
42178
+ // No bottom padding: the board row is the horizontal scroll
42179
+ // container, and a gap below it would strand its scrollbar.
42180
+ padding: "0 30px 0 30px",
42181
+ flex: 1,
42182
+ minHeight: 0,
42183
+ display: "flex",
42184
+ flexDirection: "column",
42185
+ overflow: "hidden"
42186
+ },
42187
+ children: [
42188
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { style: { padding: "20px 10px" }, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42189
+ Dropdown,
42190
+ {
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
42203
+ }
42204
+ ) }),
42205
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
42206
+ "div",
42207
+ {
42208
+ style: {
42209
+ display: "flex",
42210
+ gap: 8,
42211
+ flex: 1,
42212
+ minHeight: 0,
42213
+ overflowX: "auto",
42214
+ overflowY: "hidden"
42215
+ },
42216
+ children: [
42217
+ selectedBoard?.swimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42218
+ SwimlaneColumn,
42219
+ {
42220
+ swimlane,
42221
+ selected: false,
42222
+ selectedIssueId: selectedIssue?.id ?? null,
42223
+ commentsByIssueId,
42224
+ dragOver: dragOverSwimlaneId === swimlane.id,
42225
+ dropIndex: dropTarget?.swimlaneId === swimlane.id ? dropTarget.index : null,
42226
+ onSelectIssue: selectIssue,
42227
+ onSelectIssueComments: selectIssueComments,
42228
+ onCreateIssue: openCreateIssueModal,
42229
+ onDropIssue: moveIssue(setState, socketRef),
42230
+ onDragOver: setDragOverSwimlaneId,
42231
+ onDragOverIssue: (swimlaneId, index2) => setDropTarget({ swimlaneId, index: index2 }),
42232
+ onDragLeave: clearDragState
42233
+ },
42234
+ swimlane.id
42235
+ )),
42236
+ !(selectedIssue && state?.user) && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { style: { width: ASIDE_WIDTH, flexShrink: 0 } })
42237
+ ]
42238
+ }
42239
+ )
42240
+ ]
42241
+ }
42242
+ ),
42243
+ selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40213
42244
  IssueDetails,
40214
42245
  {
40215
42246
  whoAmI: state.user,
@@ -40223,6 +42254,8 @@ var App = () => {
40223
42254
  onAddTag: addIssueTag,
40224
42255
  onRemoveTag: removeIssueTag,
40225
42256
  onAddAssignee: addIssueAssignee,
42257
+ onAddExternalAssignee: addExternalIssueAssignee,
42258
+ onRemoveContributor: removeContributor,
40226
42259
  onRemoveAssignee: removeIssueAssignee,
40227
42260
  onAddComment: addIssueComment,
40228
42261
  onDeleteComment: deleteIssueComment,
@@ -40233,13 +42266,14 @@ var App = () => {
40233
42266
  onReopenIssue: reopenIssue,
40234
42267
  onCloseIssue: closeIssue,
40235
42268
  knownTags: state.tags ?? [],
40236
- knownAssignees: state.contributors ?? []
42269
+ knownAssignees: contributors,
42270
+ onOpenAssigneePicker: requestContributors
40237
42271
  }
40238
42272
  )
40239
42273
  ]
40240
42274
  }
40241
42275
  ),
40242
- createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
42276
+ createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40243
42277
  CreateIssueModal,
40244
42278
  {
40245
42279
  title: createIssueModal.title,
@@ -40254,14 +42288,14 @@ var App = () => {
40254
42288
  };
40255
42289
 
40256
42290
  // source/gui/client/main.tsx
40257
- var import_jsx_runtime22 = __toESM(require_jsx_runtime(), 1);
42291
+ var import_jsx_runtime33 = __toESM(require_jsx_runtime(), 1);
40258
42292
  (0, import_client.createRoot)(document.getElementById("root")).render(
40259
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(BrowserRouter, { children: /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(Routes, { children: [
40260
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Route, { path: "/", element: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(App, {}) }),
40261
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Route, { path: "/board/:boardId", element: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(App, {}) }),
40262
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Route, { path: "/board/:boardId/issue/:issueId", element: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(App, {}) }),
40263
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Route, { path: "/board/:boardId/:issueId", element: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(App, {}) }),
40264
- /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Route, { path: "*", element: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Navigate, { to: "/", replace: true }) })
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 }) })
40265
42299
  ] }) })
40266
42300
  );
40267
42301
  /*! Bundled license information: