epiq 1.4.5 → 1.5.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
@@ -22497,18 +22497,18 @@ function matchPath(pattern, pathname) {
22497
22497
  let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
22498
22498
  let captureGroups = match.slice(1);
22499
22499
  let params = compiledParams.reduce(
22500
- (memo2, { paramName, isOptional }, index2) => {
22500
+ (memo22, { paramName, isOptional }, index2) => {
22501
22501
  if (paramName === "*") {
22502
22502
  let splatValue = captureGroups[index2] || "";
22503
22503
  pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\/+$/, "$1");
22504
22504
  }
22505
22505
  const value = captureGroups[index2];
22506
22506
  if (isOptional && !value) {
22507
- memo2[paramName] = void 0;
22507
+ memo22[paramName] = void 0;
22508
22508
  } else {
22509
- memo2[paramName] = (value || "").replace(/%2F/g, "/");
22509
+ memo22[paramName] = (value || "").replace(/%2F/g, "/");
22510
22510
  }
22511
- return memo2;
22511
+ return memo22;
22512
22512
  },
22513
22513
  {}
22514
22514
  );
@@ -23610,9 +23610,9 @@ function shouldProcessLinkClick(event, target) {
23610
23610
  }
23611
23611
  function createSearchParams(init = "") {
23612
23612
  return new URLSearchParams(
23613
- typeof init === "string" || Array.isArray(init) || init instanceof URLSearchParams ? init : Object.keys(init).reduce((memo2, key) => {
23613
+ typeof init === "string" || Array.isArray(init) || init instanceof URLSearchParams ? init : Object.keys(init).reduce((memo22, key) => {
23614
23614
  let value = init[key];
23615
- return memo2.concat(
23615
+ return memo22.concat(
23616
23616
  Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]]
23617
23617
  );
23618
23618
  }, [])
@@ -24838,7 +24838,7 @@ function useViewTransitionState(to, { relative } = {}) {
24838
24838
  }
24839
24839
 
24840
24840
  // source/gui/client/App.tsx
24841
- var import_react16 = __toESM(require_react(), 1);
24841
+ var import_react18 = __toESM(require_react(), 1);
24842
24842
 
24843
24843
  // source/gui/client/components/Aside.tsx
24844
24844
  var import_react = __toESM(require_react(), 1);
@@ -24940,9 +24940,13 @@ var Button = ({
24940
24940
  );
24941
24941
  };
24942
24942
 
24943
- // source/gui/client/components/CreateIssueModal.tsx
24943
+ // source/gui/client/components/CreateNodeModal.tsx
24944
24944
  var import_jsx_runtime3 = __toESM(require_jsx_runtime(), 1);
24945
- var CreateIssueModal = ({
24945
+ var CreateNodeModal = ({
24946
+ eyebrow,
24947
+ fieldLabel,
24948
+ placeholder,
24949
+ confirmLabel = "create",
24946
24950
  title,
24947
24951
  onChangeTitle,
24948
24952
  onCreate,
@@ -24988,7 +24992,7 @@ var CreateIssueModal = ({
24988
24992
  letterSpacing: 1,
24989
24993
  textTransform: "uppercase"
24990
24994
  },
24991
- children: "New issue"
24995
+ children: eyebrow
24992
24996
  }
24993
24997
  ),
24994
24998
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -24999,7 +25003,7 @@ var CreateIssueModal = ({
24999
25003
  fontSize: 10,
25000
25004
  color: GUI_THEME.primary
25001
25005
  },
25002
- children: "title"
25006
+ children: fieldLabel
25003
25007
  }
25004
25008
  ),
25005
25009
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -25007,7 +25011,7 @@ var CreateIssueModal = ({
25007
25011
  {
25008
25012
  autoFocus: true,
25009
25013
  value: title,
25010
- placeholder: "issue name",
25014
+ placeholder,
25011
25015
  onChange: (event) => onChangeTitle(event.target.value),
25012
25016
  onKeyDown: (event) => {
25013
25017
  if (event.key === "Escape") {
@@ -25039,8 +25043,8 @@ var CreateIssueModal = ({
25039
25043
  marginTop: 20
25040
25044
  },
25041
25045
  children: [
25042
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Button, { variant: "ghost", onClick: onClose, children: "cancel" }),
25043
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Button, { variant: "primary", children: "create" })
25046
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Button, { type: "button", variant: "ghost", onClick: onClose, children: "cancel" }),
25047
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Button, { type: "submit", variant: "primary", children: confirmLabel })
25044
25048
  ]
25045
25049
  }
25046
25050
  )
@@ -25050,130 +25054,144 @@ var CreateIssueModal = ({
25050
25054
  }
25051
25055
  );
25052
25056
 
25053
- // source/gui/client/components/Dropdown.tsx
25057
+ // source/gui/client/components/AddSwimlaneColumn.tsx
25054
25058
  var import_react3 = __toESM(require_react(), 1);
25055
25059
  var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1);
25056
- var Dropdown = ({
25057
- label,
25058
- value,
25059
- items,
25060
- placeholder = "Select...",
25061
- onSelect,
25062
- testId
25063
- }) => {
25064
- const [open, setOpen] = (0, import_react3.useState)(false);
25065
- const ref = (0, import_react3.useRef)(null);
25066
- (0, import_react3.useEffect)(() => {
25067
- const close = (event) => {
25068
- if (ref.current && !ref.current.contains(event.target)) {
25069
- setOpen(false);
25070
- }
25071
- };
25072
- document.addEventListener("mousedown", close);
25073
- return () => document.removeEventListener("mousedown", close);
25074
- }, []);
25060
+ var COLUMN_WIDTH = 360;
25061
+ var AddSwimlaneColumn = ({ onClick }) => {
25062
+ const [hovered, setHovered] = (0, import_react3.useState)(false);
25075
25063
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
25076
- "div",
25064
+ "button",
25077
25065
  {
25078
- ref,
25066
+ type: "button",
25067
+ "data-testid": "add-swimlane",
25068
+ title: "Add swimlane",
25069
+ onClick,
25070
+ onMouseEnter: () => setHovered(true),
25071
+ onMouseLeave: () => setHovered(false),
25079
25072
  style: {
25080
- position: "relative",
25081
- width: "fit-content",
25073
+ appearance: "none",
25074
+ WebkitAppearance: "none",
25075
+ width: COLUMN_WIDTH,
25076
+ minWidth: COLUMN_WIDTH,
25077
+ height: "100%",
25078
+ boxSizing: "border-box",
25079
+ border: `1px dashed ${hovered ? GUI_THEME.secondary : GUI_THEME.line}`,
25080
+ borderRadius: 12,
25081
+ background: hovered ? GUI_THEME.bgHighlight : "transparent",
25082
+ color: hovered ? GUI_THEME.secondary : GUI_THEME.dim,
25082
25083
  display: "flex",
25084
+ flexDirection: "column",
25083
25085
  alignItems: "center",
25084
- gap: 12
25086
+ justifyContent: "center",
25087
+ gap: 6,
25088
+ cursor: "pointer",
25089
+ fontFamily: "inherit",
25090
+ outline: "none",
25091
+ transition: "color 120ms ease, border-color 120ms ease, background 120ms ease"
25085
25092
  },
25086
25093
  children: [
25087
- label ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: GUI_THEME.secondary, fontSize: "12px" }, children: label }) : null,
25088
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
25089
- Button,
25090
- {
25091
- variant: "ghost",
25092
- "data-testid": testId,
25093
- onClick: () => setOpen((value2) => !value2),
25094
- style: {
25095
- display: "flex",
25096
- alignItems: "center",
25097
- width: 100
25098
- },
25099
- children: [
25100
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: value?.label ?? placeholder }),
25101
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
25102
- "span",
25103
- {
25104
- style: {
25105
- marginLeft: "auto",
25106
- color: GUI_THEME.dim,
25107
- fontSize: "12px",
25108
- display: "inline-block",
25109
- transform: open ? "rotate(90deg)" : "rotate(0deg)",
25110
- transition: "transform 120ms ease"
25111
- },
25112
- children: "\u276F"
25113
- }
25114
- )
25115
- ]
25116
- }
25117
- ),
25118
- open && items.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
25119
- "div",
25120
- {
25121
- style: {
25122
- position: "absolute",
25123
- top: 28,
25124
- left: label ? 62 : 0,
25125
- minWidth: 220,
25126
- background: GUI_THEME.bg,
25127
- border: `1px solid ${GUI_THEME.line}`,
25128
- borderRadius: 8,
25129
- boxShadow: "0 12px 32px rgba(0, 0, 0, 0.35)",
25130
- padding: 6,
25131
- zIndex: 10
25132
- },
25133
- children: items.map((item) => {
25134
- const selected = item.id === value?.id;
25135
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
25136
- "button",
25137
- {
25138
- type: "button",
25139
- "data-testid": testId ? `${testId}-option` : void 0,
25140
- onClick: () => {
25141
- setOpen(false);
25142
- onSelect(item.id);
25143
- },
25144
- style: {
25145
- width: "100%",
25146
- display: "flex",
25147
- justifyContent: "space-between",
25148
- alignItems: "center",
25149
- border: "none",
25150
- background: selected ? GUI_THEME.line : "transparent",
25151
- color: selected ? GUI_THEME.accent : GUI_THEME.primary,
25152
- fontFamily: "inherit",
25153
- fontSize: 11,
25154
- textAlign: "left",
25155
- padding: "8px 10px",
25156
- borderRadius: 6,
25157
- cursor: "pointer"
25158
- },
25159
- children: [
25160
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: item.label }),
25161
- selected ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "\u2713" }) : null
25162
- ]
25163
- },
25164
- item.id
25165
- );
25166
- })
25167
- }
25168
- ) : null
25094
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: 20, lineHeight: 1 }, children: "+" }),
25095
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: 12 }, children: "new swimlane" })
25169
25096
  ]
25170
25097
  }
25171
25098
  );
25172
25099
  };
25173
25100
 
25101
+ // source/gui/client/components/ConfirmModal.tsx
25102
+ var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
25103
+ var ConfirmModal = ({
25104
+ eyebrow,
25105
+ heading: heading3,
25106
+ body,
25107
+ confirmLabel,
25108
+ onConfirm,
25109
+ onClose
25110
+ }) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
25111
+ "div",
25112
+ {
25113
+ style: {
25114
+ position: "fixed",
25115
+ inset: 0,
25116
+ background: "rgba(0, 0, 0, 0.25)",
25117
+ backdropFilter: "blur(.5px)",
25118
+ display: "flex",
25119
+ alignItems: "center",
25120
+ justifyContent: "center",
25121
+ zIndex: 1e3
25122
+ },
25123
+ onMouseDown: onClose,
25124
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
25125
+ "form",
25126
+ {
25127
+ "data-testid": "confirm-modal",
25128
+ onSubmit: (event) => {
25129
+ event.preventDefault();
25130
+ onConfirm();
25131
+ },
25132
+ onMouseDown: (event) => event.stopPropagation(),
25133
+ onKeyDown: (event) => {
25134
+ if (event.key === "Escape") onClose();
25135
+ },
25136
+ style: {
25137
+ marginTop: "-200px",
25138
+ width: 360,
25139
+ background: GUI_THEME.panel,
25140
+ border: `1px solid ${GUI_THEME.line}`,
25141
+ borderRadius: 12,
25142
+ padding: 20
25143
+ },
25144
+ children: [
25145
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
25146
+ "div",
25147
+ {
25148
+ style: {
25149
+ color: GUI_THEME.red,
25150
+ fontSize: 10,
25151
+ marginBottom: 8,
25152
+ letterSpacing: 1,
25153
+ textTransform: "uppercase"
25154
+ },
25155
+ children: eyebrow
25156
+ }
25157
+ ),
25158
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h2", { style: { margin: "0 0 10px", fontSize: 13, color: GUI_THEME.primary }, children: heading3 }),
25159
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
25160
+ "p",
25161
+ {
25162
+ style: {
25163
+ margin: 0,
25164
+ fontSize: 12,
25165
+ lineHeight: 1.7,
25166
+ color: GUI_THEME.secondary
25167
+ },
25168
+ children: body
25169
+ }
25170
+ ),
25171
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
25172
+ "div",
25173
+ {
25174
+ style: {
25175
+ display: "flex",
25176
+ justifyContent: "flex-end",
25177
+ gap: 10,
25178
+ marginTop: 20
25179
+ },
25180
+ children: [
25181
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Button, { autoFocus: true, type: "button", variant: "ghost", onClick: onClose, children: "cancel" }),
25182
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Button, { type: "submit", variant: "primary", style: { color: GUI_THEME.red }, children: confirmLabel })
25183
+ ]
25184
+ }
25185
+ )
25186
+ ]
25187
+ }
25188
+ )
25189
+ }
25190
+ );
25191
+
25174
25192
  // source/gui/client/components/Panel.tsx
25175
25193
  var import_react4 = __toESM(require_react(), 1);
25176
- var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
25194
+ var import_jsx_runtime6 = __toESM(require_jsx_runtime(), 1);
25177
25195
  var POINTER_SAMPLE_MS = 16;
25178
25196
  var Panel = ({
25179
25197
  as,
@@ -25223,7 +25241,7 @@ var Panel = ({
25223
25241
  return () => window.removeEventListener("mousemove", onMove);
25224
25242
  }, [proximityReach]);
25225
25243
  const glowStrength = active ? 1 : proximityReach !== void 0 ? proximity : hovered ? 1 : 0;
25226
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
25244
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
25227
25245
  Component4,
25228
25246
  {
25229
25247
  ...props,
@@ -25251,56 +25269,292 @@ var Panel = ({
25251
25269
  ...style
25252
25270
  },
25253
25271
  children: [
25254
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
25272
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
25273
+ "div",
25274
+ {
25275
+ ref: glowRef,
25276
+ style: {
25277
+ position: "absolute",
25278
+ inset: 0,
25279
+ borderRadius,
25280
+ padding: 1,
25281
+ pointerEvents: "none",
25282
+ opacity: glowStrength * glowOpacity,
25283
+ background: `radial-gradient(
25284
+ ${glowRadius}px circle at ${mouse.x}px ${mouse.y}px,
25285
+ ${glowColor},
25286
+ transparent 100%
25287
+ )`,
25288
+ mask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
25289
+ WebkitMask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
25290
+ maskComposite: "exclude",
25291
+ WebkitMaskComposite: "xor",
25292
+ transition: "opacity 140ms ease"
25293
+ }
25294
+ }
25295
+ ),
25296
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
25255
25297
  "div",
25256
25298
  {
25257
- ref: glowRef,
25258
25299
  style: {
25259
- position: "absolute",
25260
- inset: 0,
25261
- borderRadius,
25262
- padding: 1,
25263
- pointerEvents: "none",
25264
- opacity: glowStrength * glowOpacity,
25265
- background: `radial-gradient(
25266
- ${glowRadius}px circle at ${mouse.x}px ${mouse.y}px,
25267
- ${glowColor},
25268
- transparent 100%
25269
- )`,
25270
- mask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
25271
- WebkitMask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)",
25272
- maskComposite: "exclude",
25273
- WebkitMaskComposite: "xor",
25274
- transition: "opacity 140ms ease"
25275
- }
25300
+ position: "relative",
25301
+ zIndex: 1,
25302
+ height: "100%",
25303
+ display: "flex",
25304
+ flexDirection: "column",
25305
+ minHeight: 0
25306
+ },
25307
+ children
25308
+ }
25309
+ )
25310
+ ]
25311
+ }
25312
+ );
25313
+ };
25314
+
25315
+ // source/gui/client/components/InitProjectScreen.tsx
25316
+ var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
25317
+ var codeStyle = {
25318
+ background: GUI_THEME.panel2,
25319
+ border: `1px solid ${GUI_THEME.line}`,
25320
+ borderRadius: 6,
25321
+ padding: "2px 6px",
25322
+ color: GUI_THEME.primary
25323
+ };
25324
+ var InitProjectScreen = ({ repoRoot, message, onRetry }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
25325
+ "div",
25326
+ {
25327
+ "data-testid": "init-project-screen",
25328
+ style: {
25329
+ height: "100vh",
25330
+ background: GUI_THEME.bg,
25331
+ color: GUI_THEME.primary,
25332
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
25333
+ display: "flex",
25334
+ alignItems: "center",
25335
+ justifyContent: "center",
25336
+ padding: 30
25337
+ },
25338
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
25339
+ Panel,
25340
+ {
25341
+ proximityReach: 300,
25342
+ style: { width: 520, background: GUI_THEME.panel },
25343
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
25344
+ "div",
25345
+ {
25346
+ style: {
25347
+ padding: 28,
25348
+ display: "flex",
25349
+ flexDirection: "column",
25350
+ gap: 16
25351
+ },
25352
+ children: [
25353
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
25354
+ "div",
25355
+ {
25356
+ style: {
25357
+ color: GUI_THEME.accent,
25358
+ fontSize: 10,
25359
+ letterSpacing: 1,
25360
+ textTransform: "uppercase"
25361
+ },
25362
+ children: "Initialize project"
25363
+ }
25364
+ ),
25365
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h1", { style: { margin: 0, fontSize: 16, fontWeight: 700 }, children: "This folder is not an epiq project yet" }),
25366
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
25367
+ "div",
25368
+ {
25369
+ style: { fontSize: 12, color: GUI_THEME.secondary, lineHeight: 1.7 },
25370
+ children: [
25371
+ "No ",
25372
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: codeStyle, children: ".epiq/project.json" }),
25373
+ " was found in this directory or any of its parents:"
25374
+ ]
25375
+ }
25376
+ ),
25377
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
25378
+ "div",
25379
+ {
25380
+ style: {
25381
+ ...codeStyle,
25382
+ fontSize: 12,
25383
+ padding: "10px 12px",
25384
+ wordBreak: "break-all"
25385
+ },
25386
+ children: repoRoot
25387
+ }
25388
+ ),
25389
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
25390
+ "div",
25391
+ {
25392
+ style: { fontSize: 12, color: GUI_THEME.secondary, lineHeight: 1.7 },
25393
+ children: [
25394
+ "To start tracking issues here, run ",
25395
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: codeStyle, children: "epiq" }),
25396
+ " ",
25397
+ "in that folder and type ",
25398
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: codeStyle, children: ":init" }),
25399
+ ". That creates, commits and pushes",
25400
+ " ",
25401
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: codeStyle, children: ".epiq/project.json" }),
25402
+ ". Then come back and reload."
25403
+ ]
25404
+ }
25405
+ ),
25406
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
25407
+ "div",
25408
+ {
25409
+ style: {
25410
+ display: "flex",
25411
+ alignItems: "center",
25412
+ justifyContent: "space-between",
25413
+ gap: 12,
25414
+ marginTop: 4
25415
+ },
25416
+ children: [
25417
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontSize: 10, color: GUI_THEME.dim }, children: message }),
25418
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Button, { variant: "primary", onClick: onRetry, children: "check again" })
25419
+ ]
25420
+ }
25421
+ )
25422
+ ]
25423
+ }
25424
+ )
25425
+ }
25426
+ )
25427
+ }
25428
+ );
25429
+
25430
+ // source/gui/client/components/Dropdown.tsx
25431
+ var import_react5 = __toESM(require_react(), 1);
25432
+ var import_jsx_runtime8 = __toESM(require_jsx_runtime(), 1);
25433
+ var Dropdown = ({
25434
+ label,
25435
+ value,
25436
+ items,
25437
+ placeholder = "Select...",
25438
+ onSelect,
25439
+ testId
25440
+ }) => {
25441
+ const [open, setOpen] = (0, import_react5.useState)(false);
25442
+ const ref = (0, import_react5.useRef)(null);
25443
+ (0, import_react5.useEffect)(() => {
25444
+ const close = (event) => {
25445
+ if (ref.current && !ref.current.contains(event.target)) {
25446
+ setOpen(false);
25447
+ }
25448
+ };
25449
+ document.addEventListener("mousedown", close);
25450
+ return () => document.removeEventListener("mousedown", close);
25451
+ }, []);
25452
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
25453
+ "div",
25454
+ {
25455
+ ref,
25456
+ style: {
25457
+ position: "relative",
25458
+ width: "fit-content",
25459
+ display: "flex",
25460
+ alignItems: "center",
25461
+ gap: 12
25462
+ },
25463
+ children: [
25464
+ label ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { color: GUI_THEME.secondary, fontSize: "12px" }, children: label }) : null,
25465
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
25466
+ Button,
25467
+ {
25468
+ variant: "ghost",
25469
+ "data-testid": testId,
25470
+ onClick: () => setOpen((value2) => !value2),
25471
+ style: {
25472
+ display: "flex",
25473
+ alignItems: "center",
25474
+ width: 100
25475
+ },
25476
+ children: [
25477
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: value?.label ?? placeholder }),
25478
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
25479
+ "span",
25480
+ {
25481
+ style: {
25482
+ marginLeft: "auto",
25483
+ color: GUI_THEME.dim,
25484
+ fontSize: "12px",
25485
+ display: "inline-block",
25486
+ transform: open ? "rotate(90deg)" : "rotate(0deg)",
25487
+ transition: "transform 120ms ease"
25488
+ },
25489
+ children: "\u276F"
25490
+ }
25491
+ )
25492
+ ]
25276
25493
  }
25277
25494
  ),
25278
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
25495
+ open && items.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
25279
25496
  "div",
25280
25497
  {
25281
25498
  style: {
25282
- position: "relative",
25283
- zIndex: 1,
25284
- height: "100%",
25285
- display: "flex",
25286
- flexDirection: "column",
25287
- minHeight: 0
25499
+ position: "absolute",
25500
+ top: 28,
25501
+ left: label ? 62 : 0,
25502
+ minWidth: 220,
25503
+ background: GUI_THEME.bg,
25504
+ border: `1px solid ${GUI_THEME.line}`,
25505
+ borderRadius: 8,
25506
+ boxShadow: "0 12px 32px rgba(0, 0, 0, 0.35)",
25507
+ padding: 6,
25508
+ zIndex: 10
25288
25509
  },
25289
- children
25510
+ children: items.map((item) => {
25511
+ const selected = item.id === value?.id;
25512
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
25513
+ "button",
25514
+ {
25515
+ type: "button",
25516
+ "data-testid": testId ? `${testId}-option` : void 0,
25517
+ onClick: () => {
25518
+ setOpen(false);
25519
+ onSelect(item.id);
25520
+ },
25521
+ style: {
25522
+ width: "100%",
25523
+ display: "flex",
25524
+ justifyContent: "space-between",
25525
+ alignItems: "center",
25526
+ border: "none",
25527
+ background: selected ? GUI_THEME.line : "transparent",
25528
+ color: selected ? GUI_THEME.accent : GUI_THEME.primary,
25529
+ fontFamily: "inherit",
25530
+ fontSize: 11,
25531
+ textAlign: "left",
25532
+ padding: "8px 10px",
25533
+ borderRadius: 6,
25534
+ cursor: "pointer"
25535
+ },
25536
+ children: [
25537
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: item.label }),
25538
+ selected ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: "\u2713" }) : null
25539
+ ]
25540
+ },
25541
+ item.id
25542
+ );
25543
+ })
25290
25544
  }
25291
- )
25545
+ ) : null
25292
25546
  ]
25293
25547
  }
25294
25548
  );
25295
25549
  };
25296
25550
 
25297
25551
  // source/gui/client/components/User.tsx
25298
- var import_jsx_runtime6 = __toESM(require_jsx_runtime(), 1);
25552
+ var import_jsx_runtime9 = __toESM(require_jsx_runtime(), 1);
25299
25553
  var User = ({
25300
25554
  user,
25301
25555
  index: index2,
25302
25556
  isFocus
25303
- }) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
25557
+ }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
25304
25558
  "span",
25305
25559
  {
25306
25560
  title: user.name,
@@ -25325,19 +25579,20 @@ var User = ({
25325
25579
  );
25326
25580
 
25327
25581
  // source/version.ts
25328
- var EPIQ_VERSION = "1.4.5";
25582
+ var EPIQ_VERSION = "1.5.0";
25329
25583
 
25330
25584
  // source/gui/client/components/Header.tsx
25331
- var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
25585
+ var import_jsx_runtime10 = __toESM(require_jsx_runtime(), 1);
25332
25586
  var Header = ({
25333
25587
  state,
25334
- connected,
25588
+ connection,
25589
+ onReconnect,
25335
25590
  scrubbing,
25336
25591
  syncStatus
25337
25592
  }) => {
25338
- const syncColor = syncStatus.status === "synced" ? GUI_THEME.green : syncStatus.status === "failed" ? GUI_THEME.red : GUI_THEME.accent;
25593
+ const syncColor = syncStatus.status === "synced" ? GUI_THEME.green : syncStatus.status === "failed" ? GUI_THEME.red : syncStatus.status === "offline" ? GUI_THEME.dim : GUI_THEME.accent;
25339
25594
  const syncLabel = syncStatus.status === "failed" ? "-" : syncStatus.msg.toLowerCase();
25340
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
25595
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25341
25596
  Panel,
25342
25597
  {
25343
25598
  as: "header",
@@ -25351,7 +25606,7 @@ var Header = ({
25351
25606
  borderRight: "none",
25352
25607
  borderTop: "none"
25353
25608
  },
25354
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
25609
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25355
25610
  "div",
25356
25611
  {
25357
25612
  style: {
@@ -25362,7 +25617,7 @@ var Header = ({
25362
25617
  justifyContent: "space-between"
25363
25618
  },
25364
25619
  children: [
25365
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
25620
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25366
25621
  "div",
25367
25622
  {
25368
25623
  style: {
@@ -25373,7 +25628,7 @@ var Header = ({
25373
25628
  children: ":epiq"
25374
25629
  }
25375
25630
  ),
25376
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
25631
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25377
25632
  "div",
25378
25633
  {
25379
25634
  style: {
@@ -25382,7 +25637,7 @@ var Header = ({
25382
25637
  gap: 24
25383
25638
  },
25384
25639
  children: [
25385
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
25640
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25386
25641
  "div",
25387
25642
  {
25388
25643
  style: {
@@ -25393,11 +25648,11 @@ var Header = ({
25393
25648
  whiteSpace: "nowrap"
25394
25649
  },
25395
25650
  children: [
25396
- scrubbing && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
25397
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { color: GUI_THEME.accent, fontWeight: 700 }, children: "Read-only" }),
25398
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { color: GUI_THEME.dim }, children: "|" })
25651
+ scrubbing && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
25652
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: GUI_THEME.accent, fontWeight: 700 }, children: "Read-only" }),
25653
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: GUI_THEME.dim }, children: "|" })
25399
25654
  ] }),
25400
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
25655
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25401
25656
  "span",
25402
25657
  {
25403
25658
  style: {
@@ -25407,7 +25662,7 @@ var Header = ({
25407
25662
  justifyContent: "flex-end"
25408
25663
  },
25409
25664
  children: [
25410
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
25665
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25411
25666
  "span",
25412
25667
  {
25413
25668
  title: syncStatus.status === "failed" ? syncStatus.msg : void 0,
@@ -25419,14 +25674,14 @@ var Header = ({
25419
25674
  whiteSpace: "nowrap",
25420
25675
  textAlign: "right"
25421
25676
  },
25422
- children: connected ? syncLabel : "-"
25677
+ children: connection === "connected" ? syncLabel : "-"
25423
25678
  }
25424
25679
  ),
25425
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
25680
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25426
25681
  "span",
25427
25682
  {
25428
25683
  style: {
25429
- color: connected ? syncColor : GUI_THEME.dim,
25684
+ color: connection === "connected" ? syncColor : GUI_THEME.dim,
25430
25685
  fontSize: 4
25431
25686
  },
25432
25687
  children: "\u25CF"
@@ -25435,18 +25690,31 @@ var Header = ({
25435
25690
  ]
25436
25691
  }
25437
25692
  ),
25438
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { color: GUI_THEME.dim }, children: "|" }),
25439
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
25693
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: GUI_THEME.dim }, children: "|" }),
25694
+ connection === "connected" ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: GUI_THEME.dim }, children: "connected" }) : connection === "connecting" ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: GUI_THEME.dim }, children: "connecting\u2026" }) : connection === "reconnecting" ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25440
25695
  "span",
25441
25696
  {
25442
- style: {
25443
- color: GUI_THEME.dim
25444
- },
25445
- children: connected ? "connected" : "disconnected"
25697
+ "data-testid": "reconnecting",
25698
+ style: { color: GUI_THEME.accent },
25699
+ children: "reconnecting\u2026"
25446
25700
  }
25701
+ ) : (
25702
+ // The button alone says it: an offer to reconnect only makes
25703
+ // sense if the connection is gone.
25704
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25705
+ Button,
25706
+ {
25707
+ "data-testid": "connection-lost",
25708
+ variant: "ghost",
25709
+ onClick: onReconnect,
25710
+ title: "Not connected \u2014 reconnect now",
25711
+ style: { color: GUI_THEME.red, fontSize: 10 },
25712
+ children: "reconnect"
25713
+ }
25714
+ )
25447
25715
  ),
25448
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { color: GUI_THEME.dim }, children: "|" }),
25449
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
25716
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: GUI_THEME.dim }, children: "|" }),
25717
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25450
25718
  "span",
25451
25719
  {
25452
25720
  style: {
@@ -25458,7 +25726,7 @@ var Header = ({
25458
25726
  ]
25459
25727
  }
25460
25728
  ),
25461
- state?.user && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(User, { user: state.user })
25729
+ state?.user && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(User, { user: state.user })
25462
25730
  ]
25463
25731
  }
25464
25732
  )
@@ -25470,14 +25738,14 @@ var Header = ({
25470
25738
  };
25471
25739
 
25472
25740
  // source/gui/client/components/IssueDetails.tsx
25473
- var import_react10 = __toESM(require_react(), 1);
25741
+ var import_react11 = __toESM(require_react(), 1);
25474
25742
 
25475
25743
  // source/gui/client/components/ManageContributorsModal.tsx
25476
- var import_react5 = __toESM(require_react(), 1);
25744
+ var import_react6 = __toESM(require_react(), 1);
25477
25745
 
25478
25746
  // source/gui/client/components/IconLock.tsx
25479
- var import_jsx_runtime8 = __toESM(require_jsx_runtime(), 1);
25480
- var IconLock = ({ size = 13 }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
25747
+ var import_jsx_runtime11 = __toESM(require_jsx_runtime(), 1);
25748
+ var IconLock = ({ size = 13 }) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
25481
25749
  "svg",
25482
25750
  {
25483
25751
  width: size,
@@ -25490,16 +25758,16 @@ var IconLock = ({ size = 13 }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
25490
25758
  strokeLinejoin: "round",
25491
25759
  "aria-hidden": "true",
25492
25760
  children: [
25493
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }),
25494
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" }),
25495
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "12", cy: "16.5", r: "2.1", fill: "currentColor", stroke: "none" })
25761
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }),
25762
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" }),
25763
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("circle", { cx: "12", cy: "16.5", r: "2.1", fill: "currentColor", stroke: "none" })
25496
25764
  ]
25497
25765
  }
25498
25766
  );
25499
25767
 
25500
25768
  // source/gui/client/components/IconTrash.tsx
25501
- var import_jsx_runtime9 = __toESM(require_jsx_runtime(), 1);
25502
- var IconTrash = ({ size = 14 }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
25769
+ var import_jsx_runtime12 = __toESM(require_jsx_runtime(), 1);
25770
+ var IconTrash = ({ size = 14 }) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25503
25771
  "svg",
25504
25772
  {
25505
25773
  width: size,
@@ -25512,24 +25780,24 @@ var IconTrash = ({ size = 14 }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)
25512
25780
  strokeLinejoin: "round",
25513
25781
  "aria-hidden": "true",
25514
25782
  children: [
25515
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("polyline", { points: "3 6 5 6 21 6" }),
25516
- /* @__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" }),
25517
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "10", y1: "11", x2: "10", y2: "17" }),
25518
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("line", { x1: "14", y1: "11", x2: "14", y2: "17" })
25783
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("polyline", { points: "3 6 5 6 21 6" }),
25784
+ /* @__PURE__ */ (0, import_jsx_runtime12.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" }),
25785
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "10", y1: "11", x2: "10", y2: "17" }),
25786
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "14", y1: "11", x2: "14", y2: "17" })
25519
25787
  ]
25520
25788
  }
25521
25789
  );
25522
25790
 
25523
25791
  // source/gui/client/components/ManageContributorsModal.tsx
25524
- var import_jsx_runtime10 = __toESM(require_jsx_runtime(), 1);
25792
+ var import_jsx_runtime13 = __toESM(require_jsx_runtime(), 1);
25525
25793
  var ManageContributorsModal = ({
25526
25794
  contributors,
25527
25795
  onRemove,
25528
25796
  onClose
25529
25797
  }) => {
25530
- const [armedId, setArmedId] = (0, import_react5.useState)(null);
25798
+ const [armedId, setArmedId] = (0, import_react6.useState)(null);
25531
25799
  const sorted = [...contributors].sort((a, b) => a.name.localeCompare(b.name));
25532
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25800
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25533
25801
  "div",
25534
25802
  {
25535
25803
  style: {
@@ -25543,7 +25811,7 @@ var ManageContributorsModal = ({
25543
25811
  zIndex: 1e3
25544
25812
  },
25545
25813
  onMouseDown: onClose,
25546
- children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25814
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25547
25815
  "div",
25548
25816
  {
25549
25817
  onMouseDown: (event) => event.stopPropagation(),
@@ -25559,7 +25827,7 @@ var ManageContributorsModal = ({
25559
25827
  gap: 14
25560
25828
  },
25561
25829
  children: [
25562
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25830
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25563
25831
  "div",
25564
25832
  {
25565
25833
  style: {
@@ -25571,8 +25839,8 @@ var ManageContributorsModal = ({
25571
25839
  children: "Manage external contributors"
25572
25840
  }
25573
25841
  ),
25574
- /* @__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." }),
25575
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25842
+ /* @__PURE__ */ (0, import_jsx_runtime13.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." }),
25843
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25576
25844
  "div",
25577
25845
  {
25578
25846
  style: {
@@ -25585,7 +25853,7 @@ var ManageContributorsModal = ({
25585
25853
  children: sorted.map((contributor) => {
25586
25854
  const locked = contributor.hasAuthoredAnywhere;
25587
25855
  const armed = armedId === contributor.id;
25588
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25856
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25589
25857
  "div",
25590
25858
  {
25591
25859
  style: {
@@ -25597,7 +25865,7 @@ var ManageContributorsModal = ({
25597
25865
  borderRadius: 6
25598
25866
  },
25599
25867
  children: [
25600
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25868
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25601
25869
  "span",
25602
25870
  {
25603
25871
  style: {
@@ -25611,7 +25879,7 @@ var ManageContributorsModal = ({
25611
25879
  ]
25612
25880
  }
25613
25881
  ),
25614
- locked ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25882
+ locked ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25615
25883
  "span",
25616
25884
  {
25617
25885
  title: "Has contributed \u2014 their name is in the immutable log either way",
@@ -25623,9 +25891,9 @@ var ManageContributorsModal = ({
25623
25891
  // removable rows keep the same height.
25624
25892
  padding: "3px 6px"
25625
25893
  },
25626
- children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(IconLock, {})
25894
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(IconLock, {})
25627
25895
  }
25628
- ) : contributor.isRemoved ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { color: GUI_THEME.dim, fontSize: 10 }, children: "removed" }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25896
+ ) : contributor.isRemoved ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { style: { color: GUI_THEME.dim, fontSize: 10 }, children: "removed" }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25629
25897
  Button,
25630
25898
  {
25631
25899
  variant: "ghost",
@@ -25643,8 +25911,8 @@ var ManageContributorsModal = ({
25643
25911
  borderColor: armed ? GUI_THEME.red : void 0
25644
25912
  },
25645
25913
  children: [
25646
- armed && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { fontSize: 10 }, children: "confirm" }),
25647
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(IconTrash, {})
25914
+ armed && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { style: { fontSize: 10 }, children: "confirm" }),
25915
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(IconTrash, {})
25648
25916
  ]
25649
25917
  }
25650
25918
  )
@@ -25655,7 +25923,7 @@ var ManageContributorsModal = ({
25655
25923
  })
25656
25924
  }
25657
25925
  ),
25658
- /* @__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" }) })
25926
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { style: { display: "flex", justifyContent: "flex-end" }, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { variant: "ghost", onClick: onClose, children: "close" }) })
25659
25927
  ]
25660
25928
  }
25661
25929
  )
@@ -25664,8 +25932,8 @@ var ManageContributorsModal = ({
25664
25932
  };
25665
25933
 
25666
25934
  // source/gui/client/components/CopyRef.tsx
25667
- var import_react6 = __toESM(require_react(), 1);
25668
- var import_jsx_runtime11 = __toESM(require_jsx_runtime(), 1);
25935
+ var import_react7 = __toESM(require_react(), 1);
25936
+ var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
25669
25937
  var copyToClipboard = async (value) => {
25670
25938
  try {
25671
25939
  await navigator.clipboard.writeText(value);
@@ -25675,12 +25943,12 @@ var copyToClipboard = async (value) => {
25675
25943
  }
25676
25944
  };
25677
25945
  var CopyRef = ({ refValue }) => {
25678
- const [copied, setCopied] = (0, import_react6.useState)(false);
25679
- const resetTimer = (0, import_react6.useRef)(
25946
+ const [copied, setCopied] = (0, import_react7.useState)(false);
25947
+ const resetTimer = (0, import_react7.useRef)(
25680
25948
  void 0
25681
25949
  );
25682
- (0, import_react6.useEffect)(() => () => clearTimeout(resetTimer.current), []);
25683
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
25950
+ (0, import_react7.useEffect)(() => () => clearTimeout(resetTimer.current), []);
25951
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25684
25952
  "button",
25685
25953
  {
25686
25954
  type: "button",
@@ -25715,8 +25983,8 @@ var CopyRef = ({ refValue }) => {
25715
25983
  };
25716
25984
 
25717
25985
  // source/gui/client/components/FormHeader.tsx
25718
- var import_jsx_runtime12 = __toESM(require_jsx_runtime(), 1);
25719
- var FormHeader = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25986
+ var import_jsx_runtime15 = __toESM(require_jsx_runtime(), 1);
25987
+ var FormHeader = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25720
25988
  "div",
25721
25989
  {
25722
25990
  style: {
@@ -25731,8 +25999,8 @@ var FormHeader = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)
25731
25999
  );
25732
26000
 
25733
26001
  // source/gui/client/components/FormPrimitives.tsx
25734
- var import_jsx_runtime13 = __toESM(require_jsx_runtime(), 1);
25735
- var Input = (props) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
26002
+ var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
26003
+ var Input = (props) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
25736
26004
  "input",
25737
26005
  {
25738
26006
  ...props,
@@ -25750,7 +26018,7 @@ var Input = (props) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25750
26018
  }
25751
26019
  }
25752
26020
  );
25753
- var Textarea = (props) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
26021
+ var Textarea = (props) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
25754
26022
  "textarea",
25755
26023
  {
25756
26024
  maxLength: props.maxLength ?? 1500,
@@ -25772,28 +26040,28 @@ var Textarea = (props) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25772
26040
  }
25773
26041
  }
25774
26042
  );
25775
- var ActionRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
26043
+ var ActionRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
25776
26044
  "div",
25777
26045
  {
25778
26046
  style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 8 },
25779
26047
  children
25780
26048
  }
25781
26049
  );
25782
- var AddRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { style: { display: "flex", gap: 8, marginTop: 10 }, children });
25783
- var ChipRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10 }, children });
25784
- var Empty = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { style: { display: "inline-block", marginTop: 8, color: GUI_THEME.dim }, children });
26050
+ var AddRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { display: "flex", gap: 8, marginTop: 10 }, children });
26051
+ var ChipRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10 }, children });
26052
+ var Empty = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: { display: "inline-block", marginTop: 8, color: GUI_THEME.dim }, children });
25785
26053
 
25786
26054
  // source/gui/client/components/IssueAttachments.tsx
25787
- var import_react7 = __toESM(require_react(), 1);
26055
+ var import_react8 = __toESM(require_react(), 1);
25788
26056
 
25789
26057
  // source/gui/client/components/Section.tsx
25790
- var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
26058
+ var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
25791
26059
  var Section = ({
25792
26060
  title,
25793
26061
  action,
25794
26062
  children,
25795
26063
  first = false
25796
- }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
26064
+ }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25797
26065
  "section",
25798
26066
  {
25799
26067
  style: {
@@ -25801,7 +26069,7 @@ var Section = ({
25801
26069
  borderTop: first ? "none" : `1px solid ${GUI_THEME.line}`
25802
26070
  },
25803
26071
  children: [
25804
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
26072
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25805
26073
  "div",
25806
26074
  {
25807
26075
  style: {
@@ -25811,7 +26079,7 @@ var Section = ({
25811
26079
  gap: 12
25812
26080
  },
25813
26081
  children: [
25814
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
26082
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25815
26083
  "span",
25816
26084
  {
25817
26085
  style: {
@@ -25833,7 +26101,7 @@ var Section = ({
25833
26101
  );
25834
26102
 
25835
26103
  // source/gui/client/components/IssueAttachments.tsx
25836
- var import_jsx_runtime15 = __toESM(require_jsx_runtime(), 1);
26104
+ var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
25837
26105
  var IssueAttachments = ({
25838
26106
  issueId,
25839
26107
  readonly,
@@ -25842,15 +26110,15 @@ var IssueAttachments = ({
25842
26110
  onUploadFiles,
25843
26111
  onDeleteAttachment
25844
26112
  }) => {
25845
- const [dragging, setDragging] = (0, import_react7.useState)(false);
25846
- const [lightbox, setLightbox] = (0, import_react7.useState)(null);
25847
- const [broken, setBroken] = (0, import_react7.useState)({});
25848
- (0, import_react7.useEffect)(() => {
26113
+ const [dragging, setDragging] = (0, import_react8.useState)(false);
26114
+ const [lightbox, setLightbox] = (0, import_react8.useState)(null);
26115
+ const [broken, setBroken] = (0, import_react8.useState)({});
26116
+ (0, import_react8.useEffect)(() => {
25849
26117
  setLightbox(null);
25850
26118
  setDragging(false);
25851
26119
  setBroken({});
25852
26120
  }, [issueId]);
25853
- (0, import_react7.useEffect)(() => {
26121
+ (0, import_react8.useEffect)(() => {
25854
26122
  if (!lightbox) return;
25855
26123
  const onKeyDown = (event) => {
25856
26124
  if (event.key === "Escape") setLightbox(null);
@@ -25868,12 +26136,12 @@ var IssueAttachments = ({
25868
26136
  );
25869
26137
  if (files.length > 0) onUploadFiles?.(issueId, files);
25870
26138
  };
25871
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
26139
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
25872
26140
  Section,
25873
26141
  {
25874
26142
  title: `Attachments${attachments.length ? ` (${attachments.length})` : ""}`,
25875
26143
  children: [
25876
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
26144
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
25877
26145
  "div",
25878
26146
  {
25879
26147
  onDragOver: (event) => {
@@ -25891,7 +26159,7 @@ var IssueAttachments = ({
25891
26159
  transition: "padding 0.12s ease, border-color 0.12s ease"
25892
26160
  },
25893
26161
  children: [
25894
- 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)(
26162
+ attachments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Empty, { children: canUpload ? "No attachments \u2014 drop an image here" : "No attachments" }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
25895
26163
  "div",
25896
26164
  {
25897
26165
  style: {
@@ -25899,12 +26167,12 @@ var IssueAttachments = ({
25899
26167
  flexWrap: "wrap",
25900
26168
  gap: 8
25901
26169
  },
25902
- children: attachments.map((attachment) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
26170
+ children: attachments.map((attachment) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
25903
26171
  "div",
25904
26172
  {
25905
26173
  style: { position: "relative", width: 72, height: 72 },
25906
26174
  children: [
25907
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26175
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
25908
26176
  "button",
25909
26177
  {
25910
26178
  type: "button",
@@ -25924,7 +26192,7 @@ var IssueAttachments = ({
25924
26192
  width: "100%",
25925
26193
  height: "100%"
25926
26194
  },
25927
- children: broken[attachment.id] ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26195
+ children: broken[attachment.id] ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
25928
26196
  "span",
25929
26197
  {
25930
26198
  title: "Attachment unavailable (missing, corrupt, or over the size cap)",
@@ -25941,7 +26209,7 @@ var IssueAttachments = ({
25941
26209
  },
25942
26210
  children: "unavailable"
25943
26211
  }
25944
- ) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26212
+ ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
25945
26213
  "img",
25946
26214
  {
25947
26215
  src: `/media/${attachment.fileName}`,
@@ -25958,7 +26226,7 @@ var IssueAttachments = ({
25958
26226
  )
25959
26227
  }
25960
26228
  ),
25961
- attachment.canDelete && !readonly && onDeleteAttachment && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26229
+ attachment.canDelete && !readonly && onDeleteAttachment && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
25962
26230
  "button",
25963
26231
  {
25964
26232
  type: "button",
@@ -25988,7 +26256,7 @@ var IssueAttachments = ({
25988
26256
  ))
25989
26257
  }
25990
26258
  ),
25991
- canUpload && attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26259
+ canUpload && attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
25992
26260
  "div",
25993
26261
  {
25994
26262
  style: {
@@ -25999,7 +26267,7 @@ var IssueAttachments = ({
25999
26267
  children: "drop an image to attach"
26000
26268
  }
26001
26269
  ),
26002
- uploadStatus.state === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
26270
+ uploadStatus.state === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
26003
26271
  "div",
26004
26272
  {
26005
26273
  style: {
@@ -26014,7 +26282,7 @@ var IssueAttachments = ({
26014
26282
  ]
26015
26283
  }
26016
26284
  ),
26017
- uploadStatus.state === "error" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26285
+ uploadStatus.state === "error" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26018
26286
  "div",
26019
26287
  {
26020
26288
  style: {
@@ -26028,7 +26296,7 @@ var IssueAttachments = ({
26028
26296
  ]
26029
26297
  }
26030
26298
  ),
26031
- lightbox && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
26299
+ lightbox && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
26032
26300
  "div",
26033
26301
  {
26034
26302
  onClick: () => setLightbox(null),
@@ -26045,7 +26313,7 @@ var IssueAttachments = ({
26045
26313
  cursor: "zoom-out"
26046
26314
  },
26047
26315
  children: [
26048
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26316
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26049
26317
  "img",
26050
26318
  {
26051
26319
  src: `/media/${lightbox.fileName}`,
@@ -26058,7 +26326,7 @@ var IssueAttachments = ({
26058
26326
  }
26059
26327
  }
26060
26328
  ),
26061
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 11 }, children: [
26329
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 11 }, children: [
26062
26330
  lightbox.name,
26063
26331
  " \xB7 ",
26064
26332
  Math.max(1, Math.round(lightbox.bytes / 1024)),
@@ -26074,7 +26342,7 @@ var IssueAttachments = ({
26074
26342
  };
26075
26343
 
26076
26344
  // source/gui/client/components/IssueComments.tsx
26077
- var import_react9 = __toESM(require_react(), 1);
26345
+ var import_react10 = __toESM(require_react(), 1);
26078
26346
 
26079
26347
  // source/gui/client/lib/gui-format.helper.ts
26080
26348
  var timeAgo = (timestampMs) => {
@@ -26094,6 +26362,10 @@ var timeAgo = (timestampMs) => {
26094
26362
  }
26095
26363
  return "just now";
26096
26364
  };
26365
+ var formatAbsolute = (timestampMs) => new Date(timestampMs).toLocaleString(void 0, {
26366
+ dateStyle: "medium",
26367
+ timeStyle: "short"
26368
+ });
26097
26369
 
26098
26370
  // node_modules/devlop/lib/default.js
26099
26371
  function ok() {
@@ -27730,11 +28002,11 @@ function addChildren(props, children) {
27730
28002
  }
27731
28003
  }
27732
28004
  }
27733
- function productionCreate(_, jsx37, jsxs25) {
28005
+ function productionCreate(_, jsx42, jsxs30) {
27734
28006
  return create2;
27735
28007
  function create2(_2, type, props, key) {
27736
28008
  const isStaticChildren = Array.isArray(props.children);
27737
- const fn = isStaticChildren ? jsxs25 : jsx37;
28009
+ const fn = isStaticChildren ? jsxs30 : jsx42;
27738
28010
  return key ? fn(type, props, key) : fn(type, props);
27739
28011
  }
27740
28012
  }
@@ -27979,8 +28251,8 @@ var urlAttributes = {
27979
28251
  };
27980
28252
 
27981
28253
  // node_modules/react-markdown/lib/index.js
27982
- var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
27983
- var import_react8 = __toESM(require_react(), 1);
28254
+ var import_jsx_runtime19 = __toESM(require_jsx_runtime(), 1);
28255
+ var import_react9 = __toESM(require_react(), 1);
27984
28256
 
27985
28257
  // node_modules/mdast-util-to-string/lib/index.js
27986
28258
  var emptyOptions2 = {};
@@ -35654,11 +35926,11 @@ function post(tree, options) {
35654
35926
  }
35655
35927
  visit(tree, transform);
35656
35928
  return toJsxRuntime(tree, {
35657
- Fragment: import_jsx_runtime16.Fragment,
35929
+ Fragment: import_jsx_runtime19.Fragment,
35658
35930
  components: components2,
35659
35931
  ignoreInvalidStyle: true,
35660
- jsx: import_jsx_runtime16.jsx,
35661
- jsxs: import_jsx_runtime16.jsxs,
35932
+ jsx: import_jsx_runtime19.jsx,
35933
+ jsxs: import_jsx_runtime19.jsxs,
35662
35934
  passKeys: true,
35663
35935
  passNode: true
35664
35936
  });
@@ -38781,16 +39053,16 @@ function remarkGfm(options) {
38781
39053
  }
38782
39054
 
38783
39055
  // source/gui/client/components/MarkdownContent.tsx
38784
- var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
39056
+ var import_jsx_runtime20 = __toESM(require_jsx_runtime(), 1);
38785
39057
  var components = {
38786
- p: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("p", { style: { margin: "0 0 14px" }, children }),
38787
- ul: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ul", { style: { margin: "0 0 14px", paddingLeft: 20 }, children }),
38788
- ol: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ol", { style: { margin: "0 0 14px", paddingLeft: 20 }, children }),
38789
- li: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("li", { style: { margin: "2px 0" }, children }),
38790
- h1: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h1", { style: { fontSize: 16, margin: "0 0 14px" }, children }),
38791
- h2: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h2", { style: { fontSize: 14, margin: "0 0 14px" }, children }),
38792
- h3: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { style: { fontSize: 13, margin: "0 0 14px" }, children }),
38793
- a: ({ children, href }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39058
+ p: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("p", { style: { margin: "0 0 14px" }, children }),
39059
+ ul: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("ul", { style: { margin: "0 0 14px", paddingLeft: 20 }, children }),
39060
+ ol: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("ol", { style: { margin: "0 0 14px", paddingLeft: 20 }, children }),
39061
+ li: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("li", { style: { margin: "2px 0" }, children }),
39062
+ h1: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h1", { style: { fontSize: 16, margin: "0 0 14px" }, children }),
39063
+ h2: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h2", { style: { fontSize: 14, margin: "0 0 14px" }, children }),
39064
+ h3: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("h3", { style: { fontSize: 13, margin: "0 0 14px" }, children }),
39065
+ a: ({ children, href }) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
38794
39066
  "a",
38795
39067
  {
38796
39068
  href,
@@ -38800,7 +39072,7 @@ var components = {
38800
39072
  children
38801
39073
  }
38802
39074
  ),
38803
- blockquote: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39075
+ blockquote: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
38804
39076
  "blockquote",
38805
39077
  {
38806
39078
  style: {
@@ -38814,7 +39086,7 @@ var components = {
38814
39086
  ),
38815
39087
  code: ({ children, className }) => {
38816
39088
  const isBlock = Boolean(className);
38817
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39089
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
38818
39090
  "code",
38819
39091
  {
38820
39092
  style: {
@@ -38831,8 +39103,8 @@ var components = {
38831
39103
  }
38832
39104
  );
38833
39105
  },
38834
- pre: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("pre", { style: { margin: "0 0 14px" }, children }),
38835
- hr: () => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39106
+ pre: ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("pre", { style: { margin: "0 0 14px" }, children }),
39107
+ hr: () => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
38836
39108
  "hr",
38837
39109
  {
38838
39110
  style: {
@@ -38846,7 +39118,7 @@ var components = {
38846
39118
  var MarkdownContent = ({
38847
39119
  content: content3,
38848
39120
  softBreaks = false
38849
- }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39121
+ }) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
38850
39122
  "div",
38851
39123
  {
38852
39124
  style: {
@@ -38859,7 +39131,7 @@ var MarkdownContent = ({
38859
39131
  // collapsing, so the container doesn't end in dead whitespace.
38860
39132
  marginBottom: -14
38861
39133
  },
38862
- children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
39134
+ children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
38863
39135
  Markdown,
38864
39136
  {
38865
39137
  remarkPlugins: softBreaks ? [remarkGfm, remarkBreaks] : [remarkGfm],
@@ -38874,7 +39146,7 @@ var MarkdownContent = ({
38874
39146
  var MAX_COMMENT_LENGTH = 4e3;
38875
39147
 
38876
39148
  // source/gui/client/components/IssueComments.tsx
38877
- var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
39149
+ var import_jsx_runtime21 = __toESM(require_jsx_runtime(), 1);
38878
39150
  var IssueComments = ({
38879
39151
  whoAmI,
38880
39152
  issueId,
@@ -38883,7 +39155,7 @@ var IssueComments = ({
38883
39155
  onAddComment,
38884
39156
  onDeleteComment
38885
39157
  }) => {
38886
- const [body, setBody] = (0, import_react9.useState)("");
39158
+ const [body, setBody] = (0, import_react10.useState)("");
38887
39159
  const length = body.trim().length;
38888
39160
  const tooLong = length > MAX_COMMENT_LENGTH;
38889
39161
  const addComment = () => {
@@ -38892,8 +39164,8 @@ var IssueComments = ({
38892
39164
  onAddComment?.(issueId, nextBody);
38893
39165
  setBody("");
38894
39166
  };
38895
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { children: [
38896
- 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)(
39167
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { children: [
39168
+ comments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Empty, { children: "No comments" }) : /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
38897
39169
  "div",
38898
39170
  {
38899
39171
  style: {
@@ -38903,7 +39175,7 @@ var IssueComments = ({
38903
39175
  background: GUI_THEME.tertiary
38904
39176
  },
38905
39177
  children: [
38906
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
39178
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
38907
39179
  "div",
38908
39180
  {
38909
39181
  style: {
@@ -38913,15 +39185,15 @@ var IssueComments = ({
38913
39185
  marginBottom: 8
38914
39186
  },
38915
39187
  children: [
38916
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 11 }, children: [
39188
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 11 }, children: [
38917
39189
  comment.author.name ?? "unknown",
38918
- comment.createdAt && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { style: { color: GUI_THEME.dim2 }, children: [
39190
+ comment.createdAt && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("span", { style: { color: GUI_THEME.dim2 }, children: [
38919
39191
  " ",
38920
39192
  "\xB7 ",
38921
39193
  timeAgo(comment.createdAt)
38922
39194
  ] })
38923
39195
  ] }),
38924
- !readonly && comment.author.id === whoAmI.id && onDeleteComment && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
39196
+ !readonly && comment.author.id === whoAmI.id && onDeleteComment && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
38925
39197
  Button,
38926
39198
  {
38927
39199
  variant: "ghost",
@@ -38936,13 +39208,13 @@ var IssueComments = ({
38936
39208
  ]
38937
39209
  }
38938
39210
  ),
38939
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MarkdownContent, { content: comment.body, softBreaks: true })
39211
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(MarkdownContent, { content: comment.body, softBreaks: true })
38940
39212
  ]
38941
39213
  },
38942
39214
  comment.id
38943
39215
  )) }),
38944
- !readonly && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { style: { marginTop: 20 }, children: [
38945
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
39216
+ !readonly && /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { style: { marginTop: 20 }, children: [
39217
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
38946
39218
  Textarea,
38947
39219
  {
38948
39220
  maxLength: Number.MAX_SAFE_INTEGER,
@@ -38962,8 +39234,8 @@ var IssueComments = ({
38962
39234
  }
38963
39235
  }
38964
39236
  ),
38965
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(ActionRow, { children: [
38966
- length > MAX_COMMENT_LENGTH / 2 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
39237
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(ActionRow, { children: [
39238
+ length > MAX_COMMENT_LENGTH / 2 && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
38967
39239
  "span",
38968
39240
  {
38969
39241
  style: {
@@ -38974,19 +39246,19 @@ var IssueComments = ({
38974
39246
  children: tooLong ? `${length - MAX_COMMENT_LENGTH} over the limit` : `${MAX_COMMENT_LENGTH - length} left`
38975
39247
  }
38976
39248
  ),
38977
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Button, { disabled: tooLong, onClick: addComment, children: "comment" })
39249
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Button, { disabled: tooLong, onClick: addComment, children: "comment" })
38978
39250
  ] })
38979
39251
  ] })
38980
39252
  ] });
38981
39253
  };
38982
39254
 
38983
39255
  // source/gui/client/components/Tabs.tsx
38984
- var import_jsx_runtime19 = __toESM(require_jsx_runtime(), 1);
39256
+ var import_jsx_runtime22 = __toESM(require_jsx_runtime(), 1);
38985
39257
  var Tabs = ({
38986
39258
  activeTab,
38987
39259
  tabs,
38988
39260
  onChange
38989
- }) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
39261
+ }) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
38990
39262
  "div",
38991
39263
  {
38992
39264
  style: {
@@ -38997,7 +39269,7 @@ var Tabs = ({
38997
39269
  },
38998
39270
  children: tabs.map((tab2) => {
38999
39271
  const active = tab2.id === activeTab;
39000
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
39272
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
39001
39273
  "button",
39002
39274
  {
39003
39275
  type: "button",
@@ -39015,7 +39287,7 @@ var Tabs = ({
39015
39287
  },
39016
39288
  children: [
39017
39289
  tab2.label,
39018
- typeof tab2.count === "number" && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
39290
+ typeof tab2.count === "number" && /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
39019
39291
  "span",
39020
39292
  {
39021
39293
  style: { color: tab2.count ? GUI_THEME.secondary : GUI_THEME.dim },
@@ -39035,11 +39307,71 @@ var Tabs = ({
39035
39307
  }
39036
39308
  );
39037
39309
 
39310
+ // source/gui/client/components/IssueHistory.tsx
39311
+ var import_jsx_runtime23 = __toESM(require_jsx_runtime(), 1);
39312
+ var IssueHistory = ({
39313
+ entries,
39314
+ onHoverEvent
39315
+ }) => {
39316
+ if (entries.length === 0) {
39317
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("div", { style: { color: GUI_THEME.dim, fontSize: 12 }, children: "No history yet" });
39318
+ }
39319
+ const ordered = [...entries].reverse();
39320
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39321
+ "div",
39322
+ {
39323
+ "data-testid": "issue-history",
39324
+ onMouseLeave: () => onHoverEvent(null),
39325
+ style: { display: "flex", flexDirection: "column" },
39326
+ children: ordered.map((entry, index2) => /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
39327
+ "div",
39328
+ {
39329
+ "data-testid": "issue-history-row",
39330
+ onMouseEnter: () => onHoverEvent(entry.id),
39331
+ style: {
39332
+ display: "flex",
39333
+ alignItems: "baseline",
39334
+ gap: 14,
39335
+ padding: "8px 0",
39336
+ borderTop: index2 === 0 ? "none" : `1px solid ${GUI_THEME.line}`,
39337
+ fontSize: 12
39338
+ },
39339
+ children: [
39340
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(User, { user: entry.actor }),
39341
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39342
+ "span",
39343
+ {
39344
+ style: {
39345
+ flex: 1,
39346
+ color: GUI_THEME.primary,
39347
+ fontFamily: CONTENT_FONT
39348
+ },
39349
+ children: entry.label
39350
+ }
39351
+ ),
39352
+ /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
39353
+ "span",
39354
+ {
39355
+ title: formatAbsolute(entry.t),
39356
+ style: { color: GUI_THEME.dim, whiteSpace: "nowrap" },
39357
+ children: timeAgo(entry.t)
39358
+ }
39359
+ )
39360
+ ]
39361
+ },
39362
+ entry.id
39363
+ ))
39364
+ }
39365
+ );
39366
+ };
39367
+
39038
39368
  // source/gui/client/components/IssueDetails.tsx
39039
- var import_jsx_runtime20 = __toESM(require_jsx_runtime(), 1);
39369
+ var import_jsx_runtime24 = __toESM(require_jsx_runtime(), 1);
39040
39370
  var IssueDetails = ({
39041
39371
  whoAmI,
39042
39372
  comments,
39373
+ history,
39374
+ onHoverHistoryEvent,
39043
39375
  activeTab,
39044
39376
  onChangeTab,
39045
39377
  issue,
@@ -39064,37 +39396,27 @@ var IssueDetails = ({
39064
39396
  knownAssignees: assignees,
39065
39397
  onOpenAssigneePicker
39066
39398
  }) => {
39067
- const [title, setTitle] = (0, import_react10.useState)("");
39068
- const [description, setDescription] = (0, import_react10.useState)("");
39069
- const [tagName, setTagName] = (0, import_react10.useState)("");
39070
- const [assigneeName, setAssigneeName] = (0, import_react10.useState)("");
39071
- const [managingContributors, setManagingContributors] = (0, import_react10.useState)(false);
39072
- const [editingTitle, setEditingTitle] = (0, import_react10.useState)(false);
39073
- const [editingDescription, setEditingDescription] = (0, import_react10.useState)(false);
39074
- const [addingTag, setAddingTag] = (0, import_react10.useState)(false);
39075
- const [addingAssignee, setAddingAssignee] = (0, import_react10.useState)(false);
39076
- const panelRef = (0, import_react10.useRef)(null);
39077
- const titleTextareaRef = (0, import_react10.useRef)(null);
39399
+ const [title, setTitle] = (0, import_react11.useState)("");
39400
+ const [description, setDescription] = (0, import_react11.useState)("");
39401
+ const [tagName, setTagName] = (0, import_react11.useState)("");
39402
+ const [assigneeName, setAssigneeName] = (0, import_react11.useState)("");
39403
+ const [managingContributors, setManagingContributors] = (0, import_react11.useState)(false);
39404
+ const [editingTitle, setEditingTitle] = (0, import_react11.useState)(false);
39405
+ const [editingDescription, setEditingDescription] = (0, import_react11.useState)(false);
39406
+ const [addingTag, setAddingTag] = (0, import_react11.useState)(false);
39407
+ const [addingAssignee, setAddingAssignee] = (0, import_react11.useState)(false);
39408
+ const panelRef = (0, import_react11.useRef)(null);
39409
+ const titleTextareaRef = (0, import_react11.useRef)(null);
39078
39410
  const resizeTitleTextarea = () => {
39079
39411
  const el = titleTextareaRef.current;
39080
39412
  if (!el) return;
39081
39413
  el.style.height = "auto";
39082
39414
  el.style.height = `${el.scrollHeight}px`;
39083
39415
  };
39084
- (0, import_react10.useEffect)(() => {
39416
+ (0, import_react11.useEffect)(() => {
39085
39417
  if (editingTitle) resizeTitleTextarea();
39086
39418
  }, [editingTitle]);
39087
- (0, import_react10.useEffect)(() => {
39088
- if (!issue) return;
39089
- const handleClickOutside = (event) => {
39090
- if (panelRef.current && !panelRef.current.contains(event.target)) {
39091
- onClose();
39092
- }
39093
- };
39094
- document.addEventListener("mousedown", handleClickOutside);
39095
- return () => document.removeEventListener("mousedown", handleClickOutside);
39096
- }, [issue, onClose]);
39097
- (0, import_react10.useEffect)(() => {
39419
+ (0, import_react11.useEffect)(() => {
39098
39420
  setTitle(issue?.title ?? "");
39099
39421
  setDescription(issue?.description ?? "");
39100
39422
  setTagName("");
@@ -39107,7 +39429,8 @@ var IssueDetails = ({
39107
39429
  const disabled = !issue || issue.readonly;
39108
39430
  const tabs = [
39109
39431
  { id: "overview", label: "Overview" },
39110
- { id: "comments", label: "Comments", count: comments.length }
39432
+ { id: "comments", label: "Comments", count: comments.length },
39433
+ { id: "history", label: "Log", count: history.length }
39111
39434
  ];
39112
39435
  const saveTitle = () => {
39113
39436
  if (disabled || !issue) return setEditingTitle(false);
@@ -39159,10 +39482,10 @@ var IssueDetails = ({
39159
39482
  ).filter((assignee) => !assignee.isRemoved).sort(
39160
39483
  (a, b) => assigneeRank(a) - assigneeRank(b) || a.name.localeCompare(b.name)
39161
39484
  );
39162
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(Aside, { ref: panelRef, children: [
39163
- issue ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
39164
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(FormHeader, { children: [
39165
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39485
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(Aside, { ref: panelRef, children: [
39486
+ issue ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
39487
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(FormHeader, { children: [
39488
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39166
39489
  "span",
39167
39490
  {
39168
39491
  style: {
@@ -39171,12 +39494,12 @@ var IssueDetails = ({
39171
39494
  textTransform: "uppercase",
39172
39495
  letterSpacing: "0.08em"
39173
39496
  },
39174
- children: issue.ref && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(CopyRef, { refValue: issue.ref })
39497
+ children: issue.ref && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(CopyRef, { refValue: issue.ref })
39175
39498
  }
39176
39499
  ),
39177
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Button, { variant: "ghost", onClick: onClose, children: "\xD7" })
39500
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Button, { variant: "ghost", onClick: onClose, children: "\xD7" })
39178
39501
  ] }),
39179
- editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39502
+ editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39180
39503
  "textarea",
39181
39504
  {
39182
39505
  ref: titleTextareaRef,
@@ -39214,7 +39537,7 @@ var IssueDetails = ({
39214
39537
  marginBottom: 20
39215
39538
  }
39216
39539
  }
39217
- ) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39540
+ ) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39218
39541
  "div",
39219
39542
  {
39220
39543
  onClick: () => !issue.readonly && setEditingTitle(true),
@@ -39230,14 +39553,30 @@ var IssueDetails = ({
39230
39553
  children: issue.title
39231
39554
  }
39232
39555
  ),
39233
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Tabs, { tabs, activeTab, onChange: onChangeTab }),
39234
- activeTab === "overview" && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
39235
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39556
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Tabs, { tabs, activeTab, onChange: onChangeTab }),
39557
+ activeTab === "overview" && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
39558
+ issue.createdAt > 0 && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
39559
+ "div",
39560
+ {
39561
+ "data-testid": "issue-created-at",
39562
+ title: formatAbsolute(issue.createdAt),
39563
+ style: {
39564
+ fontSize: 11,
39565
+ color: GUI_THEME.dim,
39566
+ marginBottom: 14
39567
+ },
39568
+ children: [
39569
+ "Created ",
39570
+ timeAgo(issue.createdAt)
39571
+ ]
39572
+ }
39573
+ ),
39574
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39236
39575
  Section,
39237
39576
  {
39238
39577
  first: true,
39239
39578
  title: "Description",
39240
- action: !issue.readonly && !editingDescription && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39579
+ action: !issue.readonly && !editingDescription && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39241
39580
  Button,
39242
39581
  {
39243
39582
  variant: "ghost",
@@ -39245,8 +39584,8 @@ var IssueDetails = ({
39245
39584
  children: "edit"
39246
39585
  }
39247
39586
  ),
39248
- children: editingDescription ? /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(import_jsx_runtime20.Fragment, { children: [
39249
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39587
+ children: editingDescription ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
39588
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39250
39589
  Textarea,
39251
39590
  {
39252
39591
  value: description,
@@ -39268,11 +39607,11 @@ var IssueDetails = ({
39268
39607
  }
39269
39608
  }
39270
39609
  ),
39271
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(ActionRow, { children: [
39272
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Button, { onClick: saveDescription, children: "save" }),
39273
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Button, { variant: "ghost", onClick: cancelDescription, children: "cancel" })
39610
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(ActionRow, { children: [
39611
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Button, { onClick: saveDescription, children: "save" }),
39612
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Button, { variant: "ghost", onClick: cancelDescription, children: "cancel" })
39274
39613
  ] })
39275
- ] }) : issue.description ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39614
+ ] }) : issue.description ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39276
39615
  "div",
39277
39616
  {
39278
39617
  style: {
@@ -39283,18 +39622,18 @@ var IssueDetails = ({
39283
39622
  background: GUI_THEME.tertiary,
39284
39623
  borderRadius: 8
39285
39624
  },
39286
- children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(MarkdownContent, { content: issue.description })
39625
+ children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(MarkdownContent, { content: issue.description })
39287
39626
  }
39288
- ) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Empty, { children: "No description" })
39627
+ ) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Empty, { children: "No description" })
39289
39628
  }
39290
39629
  ),
39291
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39630
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
39292
39631
  Section,
39293
39632
  {
39294
39633
  title: "Tags",
39295
- 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: "-" }),
39634
+ action: !issue.readonly && !addingTag && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Button, { variant: "ghost", onClick: () => setAddingTag(true), children: "+" }) || addingTag && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Button, { variant: "ghost", onClick: () => setAddingTag(false), children: "-" }),
39296
39635
  children: [
39297
- /* @__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)(
39636
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ChipRow, { children: issue.tags.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Empty, { children: "No tags" }) : issue.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
39298
39637
  Button,
39299
39638
  {
39300
39639
  variant: "chip",
@@ -39310,7 +39649,7 @@ var IssueDetails = ({
39310
39649
  },
39311
39650
  tag.id
39312
39651
  )) }),
39313
- addingTag && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39652
+ addingTag && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
39314
39653
  Button,
39315
39654
  {
39316
39655
  variant: "chip",
@@ -39325,8 +39664,8 @@ var IssueDetails = ({
39325
39664
  },
39326
39665
  tag.id
39327
39666
  )) }),
39328
- addingTag && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(AddRow, { children: [
39329
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39667
+ addingTag && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(AddRow, { children: [
39668
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39330
39669
  Input,
39331
39670
  {
39332
39671
  value: tagName,
@@ -39342,16 +39681,16 @@ var IssueDetails = ({
39342
39681
  }
39343
39682
  }
39344
39683
  ),
39345
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Button, { onClick: addTag, children: "add" })
39684
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Button, { onClick: addTag, children: "add" })
39346
39685
  ] })
39347
39686
  ]
39348
39687
  }
39349
39688
  ),
39350
- /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39689
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
39351
39690
  Section,
39352
39691
  {
39353
39692
  title: "Assignees",
39354
- action: !issue.readonly && !addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39693
+ action: !issue.readonly && !addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39355
39694
  Button,
39356
39695
  {
39357
39696
  variant: "ghost",
@@ -39361,7 +39700,7 @@ var IssueDetails = ({
39361
39700
  },
39362
39701
  children: "+"
39363
39702
  }
39364
- ) || addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39703
+ ) || addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39365
39704
  Button,
39366
39705
  {
39367
39706
  variant: "ghost",
@@ -39370,7 +39709,7 @@ var IssueDetails = ({
39370
39709
  }
39371
39710
  ),
39372
39711
  children: [
39373
- /* @__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)(
39712
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ChipRow, { children: issue.assignees.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Empty, { children: "No assignees" }) : issue.assignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
39374
39713
  Button,
39375
39714
  {
39376
39715
  variant: "chip",
@@ -39387,7 +39726,7 @@ var IssueDetails = ({
39387
39726
  },
39388
39727
  assignee.id
39389
39728
  )) }),
39390
- addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ChipRow, { children: availableAssignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
39729
+ addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ChipRow, { children: availableAssignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
39391
39730
  Button,
39392
39731
  {
39393
39732
  variant: "chip",
@@ -39410,7 +39749,7 @@ var IssueDetails = ({
39410
39749
  )) }),
39411
39750
  addingAssignee && assignees.some(
39412
39751
  (a) => !a.hasAuthoredAnywhere && !a.isRemoved
39413
- ) && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ChipRow, { children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39752
+ ) && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(ChipRow, { children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39414
39753
  Button,
39415
39754
  {
39416
39755
  variant: "ghost",
@@ -39420,8 +39759,8 @@ var IssueDetails = ({
39420
39759
  children: "manage contributors\u2026"
39421
39760
  }
39422
39761
  ) }),
39423
- addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(AddRow, { children: [
39424
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39762
+ addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(AddRow, { children: [
39763
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39425
39764
  Input,
39426
39765
  {
39427
39766
  value: assigneeName,
@@ -39437,7 +39776,7 @@ var IssueDetails = ({
39437
39776
  }
39438
39777
  }
39439
39778
  ),
39440
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39779
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39441
39780
  Button,
39442
39781
  {
39443
39782
  onClick: addAssignee,
@@ -39449,7 +39788,7 @@ var IssueDetails = ({
39449
39788
  ]
39450
39789
  }
39451
39790
  ),
39452
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39791
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39453
39792
  IssueAttachments,
39454
39793
  {
39455
39794
  issueId: issue.id,
@@ -39460,16 +39799,16 @@ var IssueDetails = ({
39460
39799
  onDeleteAttachment
39461
39800
  }
39462
39801
  ),
39463
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39802
+ /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39464
39803
  Section,
39465
39804
  {
39466
39805
  title: "Actions",
39467
- 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" })),
39806
+ action: !issue.readonly && (issue.isClosed ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Button, { onClick: () => onReopenIssue(issue.id), children: "reopen issue" }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Button, { onClick: () => onCloseIssue(issue.id), children: "close issue" })),
39468
39807
  children: ""
39469
39808
  }
39470
39809
  )
39471
39810
  ] }),
39472
- activeTab === "comments" && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39811
+ activeTab === "comments" && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39473
39812
  IssueComments,
39474
39813
  {
39475
39814
  whoAmI,
@@ -39479,9 +39818,16 @@ var IssueDetails = ({
39479
39818
  onAddComment,
39480
39819
  onDeleteComment
39481
39820
  }
39821
+ ),
39822
+ activeTab === "history" && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39823
+ IssueHistory,
39824
+ {
39825
+ entries: history,
39826
+ onHoverEvent: onHoverHistoryEvent
39827
+ }
39482
39828
  )
39483
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Empty, { children: "Select an issue" }),
39484
- managingContributors && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
39829
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Empty, { children: "Select an issue" }),
39830
+ managingContributors && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
39485
39831
  ManageContributorsModal,
39486
39832
  {
39487
39833
  contributors: assignees,
@@ -39493,7 +39839,7 @@ var IssueDetails = ({
39493
39839
  };
39494
39840
 
39495
39841
  // source/gui/client/components/BulkDetails.tsx
39496
- var import_jsx_runtime21 = __toESM(require_jsx_runtime(), 1);
39842
+ var import_jsx_runtime25 = __toESM(require_jsx_runtime(), 1);
39497
39843
  var countBy = (issues, pick) => {
39498
39844
  const byId = /* @__PURE__ */ new Map();
39499
39845
  for (const issue of issues) {
@@ -39504,6 +39850,7 @@ var countBy = (issues, pick) => {
39504
39850
  }
39505
39851
  return [...byId.values()];
39506
39852
  };
39853
+ var ticketWord = (count) => count === 1 ? "ticket" : "tickets";
39507
39854
  var BulkDetails = ({
39508
39855
  issues,
39509
39856
  knownTags,
@@ -39521,6 +39868,8 @@ var BulkDetails = ({
39521
39868
  onClear
39522
39869
  }) => {
39523
39870
  const total = issues.length;
39871
+ const openCount = issues.filter((issue) => !issue.isClosed).length;
39872
+ const closedCount = total - openCount;
39524
39873
  const tags = countBy(issues, (issue) => issue.tags);
39525
39874
  const assignees = countBy(issues, (issue) => issue.assignees);
39526
39875
  const availableTags = knownTags.filter(
@@ -39532,8 +39881,8 @@ var BulkDetails = ({
39532
39881
  )
39533
39882
  );
39534
39883
  const partial = (count) => count === total ? "" : ` ${count}/${total}`;
39535
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Aside, { children: [
39536
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
39884
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(Aside, { children: [
39885
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
39537
39886
  "div",
39538
39887
  {
39539
39888
  style: {
@@ -39544,16 +39893,16 @@ var BulkDetails = ({
39544
39893
  marginBottom: 12
39545
39894
  },
39546
39895
  children: [
39547
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { style: { fontSize: 13, fontWeight: 600, color: GUI_THEME.primary }, children: [
39896
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { style: { fontSize: 13, fontWeight: 600, color: GUI_THEME.primary }, children: [
39548
39897
  total,
39549
39898
  " tickets selected"
39550
39899
  ] }),
39551
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Button, { variant: "ghost", onClick: onClear, children: "clear" })
39900
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Button, { variant: "ghost", onClick: onClear, children: "clear" })
39552
39901
  ]
39553
39902
  }
39554
39903
  ),
39555
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Section, { title: "Tags", children: [
39556
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ChipRow, { children: tags.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Empty, { children: "No tags" }) : tags.map(({ item, count }) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
39904
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(Section, { title: "Tags", children: [
39905
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ChipRow, { children: tags.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Empty, { children: "No tags" }) : tags.map(({ item, count }) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
39557
39906
  Button,
39558
39907
  {
39559
39908
  variant: "chip",
@@ -39568,7 +39917,7 @@ var BulkDetails = ({
39568
39917
  },
39569
39918
  item.id
39570
39919
  )) }),
39571
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
39920
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
39572
39921
  Button,
39573
39922
  {
39574
39923
  variant: "chip",
@@ -39582,8 +39931,8 @@ var BulkDetails = ({
39582
39931
  },
39583
39932
  tag.id
39584
39933
  )) }),
39585
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(AddRow, { children: [
39586
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
39934
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(AddRow, { children: [
39935
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
39587
39936
  Input,
39588
39937
  {
39589
39938
  value: tagName,
@@ -39596,11 +39945,11 @@ var BulkDetails = ({
39596
39945
  }
39597
39946
  }
39598
39947
  ),
39599
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Button, { onClick: () => tagName.trim() && onAddTag(tagName.trim()), children: "add" })
39948
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Button, { onClick: () => tagName.trim() && onAddTag(tagName.trim()), children: "add" })
39600
39949
  ] })
39601
39950
  ] }),
39602
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Section, { title: "Assignees", children: [
39603
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ChipRow, { children: assignees.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Empty, { children: "No assignees" }) : assignees.map(({ item, count }) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
39951
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(Section, { title: "Assignees", children: [
39952
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ChipRow, { children: assignees.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Empty, { children: "No assignees" }) : assignees.map(({ item, count }) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
39604
39953
  Button,
39605
39954
  {
39606
39955
  variant: "chip",
@@ -39616,7 +39965,7 @@ var BulkDetails = ({
39616
39965
  },
39617
39966
  item.id
39618
39967
  )) }),
39619
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(ChipRow, { children: availableAssignees.map((person) => /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
39968
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(ChipRow, { children: availableAssignees.map((person) => /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
39620
39969
  Button,
39621
39970
  {
39622
39971
  variant: "chip",
@@ -39630,7 +39979,7 @@ var BulkDetails = ({
39630
39979
  },
39631
39980
  person.id
39632
39981
  )) }),
39633
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(AddRow, { children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
39982
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(AddRow, { children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
39634
39983
  Input,
39635
39984
  {
39636
39985
  value: assigneeName,
@@ -39639,30 +39988,80 @@ var BulkDetails = ({
39639
39988
  }
39640
39989
  ) })
39641
39990
  ] }),
39642
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Section, { title: "Actions", children: /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(ChipRow, { children: [
39643
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { onClick: onCloseIssues, children: [
39991
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Section, { title: "Actions", children: /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(ChipRow, { children: [
39992
+ openCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(Button, { onClick: onCloseIssues, children: [
39644
39993
  "close ",
39645
- total,
39646
- " tickets"
39994
+ openCount,
39995
+ " ",
39996
+ ticketWord(openCount)
39647
39997
  ] }),
39648
- /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(Button, { onClick: onReopenIssues, children: [
39998
+ closedCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(Button, { onClick: onReopenIssues, children: [
39649
39999
  "reopen ",
39650
- total,
39651
- " tickets"
40000
+ closedCount,
40001
+ " ",
40002
+ ticketWord(closedCount)
39652
40003
  ] })
39653
40004
  ] }) })
39654
40005
  ] });
39655
40006
  };
39656
40007
 
39657
40008
  // source/gui/client/components/SwimlaneColumn.tsx
39658
- var import_react12 = __toESM(require_react(), 1);
40009
+ var import_react14 = __toESM(require_react(), 1);
39659
40010
 
39660
40011
  // source/gui/client/components/TicketCard.tsx
39661
- var import_react11 = __toESM(require_react(), 1);
40012
+ var import_react12 = __toESM(require_react(), 1);
40013
+
40014
+ // source/gui/client/lib/gui-move-swimlane.ts
40015
+ var SWIMLANE_DRAG_TYPE = "application/x-epiq-swimlane";
40016
+ var isSwimlaneDrag = (dataTransfer) => Boolean(dataTransfer?.types.includes(SWIMLANE_DRAG_TYPE));
40017
+ var toMovePosition = (siblings, targetIndex) => {
40018
+ if (siblings.length === 0) return { at: "start" };
40019
+ if (targetIndex >= siblings.length) return { at: "end" };
40020
+ return { at: "before", sibling: siblings[Math.max(0, targetIndex)].id };
40021
+ };
40022
+ var reorder = (swimlanes, swimlaneId, targetIndex) => {
40023
+ const moved = swimlanes.find((swimlane) => swimlane.id === swimlaneId);
40024
+ if (!moved) return swimlanes;
40025
+ const without = swimlanes.filter((swimlane) => swimlane.id !== swimlaneId);
40026
+ const index2 = Math.max(0, Math.min(targetIndex, without.length));
40027
+ return [...without.slice(0, index2), moved, ...without.slice(index2)];
40028
+ };
40029
+ var moveSwimlane = (state, setState, send) => (swimlaneId, boardId, targetIndex) => {
40030
+ if (!state) return;
40031
+ const board = state.boards.find((candidate) => candidate.id === boardId);
40032
+ if (!board) return;
40033
+ const currentIndex = board.swimlanes.findIndex(
40034
+ (swimlane) => swimlane.id === swimlaneId
40035
+ );
40036
+ if (currentIndex === -1) return;
40037
+ if (targetIndex === currentIndex || targetIndex === currentIndex + 1)
40038
+ return;
40039
+ const withoutMoved = board.swimlanes.filter(
40040
+ (swimlane) => swimlane.id !== swimlaneId
40041
+ );
40042
+ const indexWithoutMoved = targetIndex > currentIndex ? targetIndex - 1 : targetIndex;
40043
+ const position3 = toMovePosition(withoutMoved, indexWithoutMoved);
40044
+ setState(
40045
+ (current) => current ? {
40046
+ ...current,
40047
+ boards: current.boards.map(
40048
+ (candidate) => candidate.id === boardId ? {
40049
+ ...candidate,
40050
+ swimlanes: reorder(
40051
+ candidate.swimlanes,
40052
+ swimlaneId,
40053
+ indexWithoutMoved
40054
+ )
40055
+ } : candidate
40056
+ )
40057
+ } : current
40058
+ );
40059
+ send("swimlane:move", { swimlaneId, boardId, position: position3 });
40060
+ };
39662
40061
 
39663
40062
  // source/gui/client/components/IconComment.tsx
39664
- var import_jsx_runtime22 = __toESM(require_jsx_runtime(), 1);
39665
- var IconComment = ({ size = 14 }) => /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
40063
+ var import_jsx_runtime26 = __toESM(require_jsx_runtime(), 1);
40064
+ var IconComment = ({ size = 14 }) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
39666
40065
  "svg",
39667
40066
  {
39668
40067
  width: size,
@@ -39674,7 +40073,7 @@ var IconComment = ({ size = 14 }) => /* @__PURE__ */ (0, import_jsx_runtime22.js
39674
40073
  strokeLinecap: "round",
39675
40074
  strokeLinejoin: "round",
39676
40075
  "aria-hidden": "true",
39677
- children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
40076
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
39678
40077
  "path",
39679
40078
  {
39680
40079
  fill: "currentColor",
@@ -39685,7 +40084,7 @@ var IconComment = ({ size = 14 }) => /* @__PURE__ */ (0, import_jsx_runtime22.js
39685
40084
  );
39686
40085
 
39687
40086
  // source/gui/client/components/TicketCard.tsx
39688
- var import_jsx_runtime23 = __toESM(require_jsx_runtime(), 1);
40087
+ var import_jsx_runtime27 = __toESM(require_jsx_runtime(), 1);
39689
40088
  var TicketCard = ({
39690
40089
  ticket,
39691
40090
  index: index2,
@@ -39697,8 +40096,8 @@ var TicketCard = ({
39697
40096
  onDragOverIssue,
39698
40097
  onDropIssueAt
39699
40098
  }) => {
39700
- const cardRef = (0, import_react11.useRef)(null);
39701
- (0, import_react11.useEffect)(() => {
40099
+ const cardRef = (0, import_react12.useRef)(null);
40100
+ (0, import_react12.useEffect)(() => {
39702
40101
  if (!isSelected) return;
39703
40102
  cardRef.current?.scrollIntoView({
39704
40103
  behavior: "smooth",
@@ -39707,7 +40106,7 @@ var TicketCard = ({
39707
40106
  });
39708
40107
  }, [isSelected]);
39709
40108
  const getVisualTargetIndex = (isAfterMiddle) => index2 + (isAfterMiddle ? 1 : 0);
39710
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
40109
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
39711
40110
  "div",
39712
40111
  {
39713
40112
  ref: cardRef,
@@ -39721,6 +40120,7 @@ var TicketCard = ({
39721
40120
  event.dataTransfer.setData("text/plain", ticket.id);
39722
40121
  },
39723
40122
  onDragOver: (event) => {
40123
+ if (isSwimlaneDrag(event.dataTransfer)) return;
39724
40124
  event.preventDefault();
39725
40125
  event.dataTransfer.dropEffect = "move";
39726
40126
  const rect = event.currentTarget.getBoundingClientRect();
@@ -39728,6 +40128,7 @@ var TicketCard = ({
39728
40128
  onDragOverIssue(getVisualTargetIndex(isAfterMiddle));
39729
40129
  },
39730
40130
  onDrop: (event) => {
40131
+ if (isSwimlaneDrag(event.dataTransfer)) return;
39731
40132
  event.preventDefault();
39732
40133
  event.stopPropagation();
39733
40134
  const issueId = event.dataTransfer.getData("text/plain");
@@ -39748,14 +40149,10 @@ var TicketCard = ({
39748
40149
  minHeight: "58px",
39749
40150
  borderRadius: "8px",
39750
40151
  marginBottom: 4,
39751
- border: `1px solid ${isSelected || isPicked ? GUI_THEME.accent : "transparent"}`,
39752
- // Only the multi-selection is outlined, so it stays legible when the
39753
- // details panel is closed.
39754
- outline: isPicked ? `1px solid ${GUI_THEME.accent}` : void 0,
39755
- outlineOffset: 1
40152
+ border: `1px solid ${isSelected || isPicked ? GUI_THEME.accent : "transparent"}`
39756
40153
  },
39757
40154
  children: [
39758
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40155
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
39759
40156
  "div",
39760
40157
  {
39761
40158
  style: {
@@ -39769,7 +40166,7 @@ var TicketCard = ({
39769
40166
  children: isSelected ? "\u276F" : index2 + 1
39770
40167
  }
39771
40168
  ),
39772
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
40169
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
39773
40170
  "div",
39774
40171
  {
39775
40172
  style: {
@@ -39781,7 +40178,7 @@ var TicketCard = ({
39781
40178
  paddingLeft: 8
39782
40179
  },
39783
40180
  children: [
39784
- /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
40181
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
39785
40182
  "div",
39786
40183
  {
39787
40184
  style: {
@@ -39792,7 +40189,7 @@ var TicketCard = ({
39792
40189
  flex: 1
39793
40190
  },
39794
40191
  children: [
39795
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40192
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
39796
40193
  "div",
39797
40194
  {
39798
40195
  style: {
@@ -39808,17 +40205,17 @@ var TicketCard = ({
39808
40205
  children: ticket.title
39809
40206
  }
39810
40207
  ),
39811
- ticket.ref && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40208
+ ticket.ref && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
39812
40209
  "div",
39813
40210
  {
39814
40211
  style: {
39815
40212
  color: GUI_THEME.dim2,
39816
40213
  fontSize: 10
39817
40214
  },
39818
- children: /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(CopyRef, { refValue: ticket.ref })
40215
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(CopyRef, { refValue: ticket.ref })
39819
40216
  }
39820
40217
  ),
39821
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40218
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
39822
40219
  "div",
39823
40220
  {
39824
40221
  style: {
@@ -39827,7 +40224,7 @@ var TicketCard = ({
39827
40224
  gap: 6,
39828
40225
  alignItems: "center"
39829
40226
  },
39830
- children: ticket.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40227
+ children: ticket.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
39831
40228
  "span",
39832
40229
  {
39833
40230
  style: {
@@ -39847,7 +40244,7 @@ var TicketCard = ({
39847
40244
  ]
39848
40245
  }
39849
40246
  ),
39850
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40247
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
39851
40248
  "div",
39852
40249
  {
39853
40250
  style: {
@@ -39857,7 +40254,7 @@ var TicketCard = ({
39857
40254
  flexShrink: 0,
39858
40255
  paddingTop: 2
39859
40256
  },
39860
- children: commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime23.jsxs)(
40257
+ children: commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
39861
40258
  "button",
39862
40259
  {
39863
40260
  type: "button",
@@ -39895,14 +40292,14 @@ var TicketCard = ({
39895
40292
  event.currentTarget.style.transform = "translateY(0)";
39896
40293
  },
39897
40294
  children: [
39898
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(IconComment, {}),
39899
- /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { children: commentCount })
40295
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(IconComment, {}),
40296
+ /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("span", { children: commentCount })
39900
40297
  ]
39901
40298
  }
39902
40299
  )
39903
40300
  }
39904
40301
  ),
39905
- ticket.assignees.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40302
+ ticket.assignees.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
39906
40303
  "div",
39907
40304
  {
39908
40305
  style: {
@@ -39912,7 +40309,7 @@ var TicketCard = ({
39912
40309
  paddingTop: 2,
39913
40310
  marginTop: "-4px"
39914
40311
  },
39915
- children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
40312
+ children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
39916
40313
  User,
39917
40314
  {
39918
40315
  user: assignee,
@@ -39931,8 +40328,130 @@ var TicketCard = ({
39931
40328
  );
39932
40329
  };
39933
40330
 
40331
+ // source/gui/client/components/KebabMenu.tsx
40332
+ var import_react13 = __toESM(require_react(), 1);
40333
+ var import_jsx_runtime28 = __toESM(require_jsx_runtime(), 1);
40334
+ var KebabMenu = ({
40335
+ items,
40336
+ testId
40337
+ }) => {
40338
+ const [open, setOpen] = (0, import_react13.useState)(false);
40339
+ const [hovered, setHovered] = (0, import_react13.useState)(false);
40340
+ const ref = (0, import_react13.useRef)(null);
40341
+ (0, import_react13.useEffect)(() => {
40342
+ if (!open) return;
40343
+ const close = (event) => {
40344
+ if (ref.current && !ref.current.contains(event.target)) {
40345
+ setOpen(false);
40346
+ }
40347
+ };
40348
+ const onEscape = (event) => {
40349
+ if (event.key === "Escape") setOpen(false);
40350
+ };
40351
+ document.addEventListener("mousedown", close);
40352
+ document.addEventListener("keydown", onEscape);
40353
+ return () => {
40354
+ document.removeEventListener("mousedown", close);
40355
+ document.removeEventListener("keydown", onEscape);
40356
+ };
40357
+ }, [open]);
40358
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)("div", { ref, style: { position: "relative", display: "flex" }, children: [
40359
+ /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
40360
+ "button",
40361
+ {
40362
+ type: "button",
40363
+ "data-testid": testId,
40364
+ title: "Swimlane actions",
40365
+ "aria-haspopup": "menu",
40366
+ "aria-expanded": open,
40367
+ onClick: () => setOpen((value) => !value),
40368
+ onMouseEnter: () => setHovered(true),
40369
+ onMouseLeave: () => setHovered(false),
40370
+ style: {
40371
+ appearance: "none",
40372
+ WebkitAppearance: "none",
40373
+ background: hovered ? "rgba(255,255,255,0.04)" : "transparent",
40374
+ border: `1px solid ${open || hovered ? GUI_THEME.line : "transparent"}`,
40375
+ borderRadius: 6,
40376
+ color: open || hovered ? GUI_THEME.secondary : GUI_THEME.dim,
40377
+ cursor: "pointer",
40378
+ fontFamily: "inherit",
40379
+ fontSize: 12,
40380
+ lineHeight: 1,
40381
+ padding: "3px 6px",
40382
+ outline: "none",
40383
+ transition: "color 120ms ease, background 120ms ease"
40384
+ },
40385
+ children: "\u22EE"
40386
+ }
40387
+ ),
40388
+ open && /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
40389
+ "div",
40390
+ {
40391
+ role: "menu",
40392
+ style: {
40393
+ position: "absolute",
40394
+ top: 26,
40395
+ right: 0,
40396
+ minWidth: 140,
40397
+ background: GUI_THEME.bg,
40398
+ border: `1px solid ${GUI_THEME.line}`,
40399
+ borderRadius: 8,
40400
+ boxShadow: "0 12px 32px rgba(0, 0, 0, 0.35)",
40401
+ padding: 6,
40402
+ zIndex: 20
40403
+ },
40404
+ children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
40405
+ MenuItem,
40406
+ {
40407
+ item,
40408
+ testId: testId ? `${testId}-${item.id}` : void 0,
40409
+ onDone: () => setOpen(false)
40410
+ },
40411
+ item.id
40412
+ ))
40413
+ }
40414
+ )
40415
+ ] });
40416
+ };
40417
+ var MenuItem = ({
40418
+ item,
40419
+ testId,
40420
+ onDone
40421
+ }) => {
40422
+ const [hovered, setHovered] = (0, import_react13.useState)(false);
40423
+ const color2 = item.danger ? GUI_THEME.red : GUI_THEME.primary;
40424
+ return /* @__PURE__ */ (0, import_jsx_runtime28.jsx)(
40425
+ "button",
40426
+ {
40427
+ type: "button",
40428
+ role: "menuitem",
40429
+ "data-testid": testId,
40430
+ onClick: () => {
40431
+ onDone();
40432
+ item.onSelect();
40433
+ },
40434
+ onMouseEnter: () => setHovered(true),
40435
+ onMouseLeave: () => setHovered(false),
40436
+ style: {
40437
+ width: "100%",
40438
+ border: "none",
40439
+ background: hovered ? GUI_THEME.line : "transparent",
40440
+ color: color2,
40441
+ fontFamily: "inherit",
40442
+ fontSize: 11,
40443
+ textAlign: "left",
40444
+ padding: "8px 10px",
40445
+ borderRadius: 6,
40446
+ cursor: "pointer"
40447
+ },
40448
+ children: item.label
40449
+ }
40450
+ );
40451
+ };
40452
+
39934
40453
  // source/gui/client/components/SwimlaneColumn.tsx
39935
- var import_jsx_runtime24 = __toESM(require_jsx_runtime(), 1);
40454
+ var import_jsx_runtime29 = __toESM(require_jsx_runtime(), 1);
39936
40455
  var COLUMN_GLOW_COLOR = "rgb(140, 176, 232)";
39937
40456
  var COLUMN_PADDING = 14;
39938
40457
  var SCROLLBAR_GUTTER_INSET = COLUMN_PADDING / 2;
@@ -39947,12 +40466,18 @@ var SwimlaneColumn = ({
39947
40466
  onSelectIssue,
39948
40467
  pickedIssueIds,
39949
40468
  onCreateIssue,
40469
+ onRenameSwimlane,
40470
+ onDeleteSwimlane,
40471
+ dropSide,
40472
+ onSwimlaneDragOver,
40473
+ onSwimlaneDragEnd,
40474
+ onDropSwimlane,
39950
40475
  onDropIssue,
39951
40476
  onDragOver,
39952
40477
  onDragOverIssue,
39953
40478
  onDragLeave
39954
40479
  }) => {
39955
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
40480
+ return /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
39956
40481
  Panel,
39957
40482
  {
39958
40483
  as: "section",
@@ -39979,6 +40504,11 @@ var SwimlaneColumn = ({
39979
40504
  onDragOver: (event) => {
39980
40505
  event.preventDefault();
39981
40506
  event.dataTransfer.dropEffect = "move";
40507
+ if (isSwimlaneDrag(event.dataTransfer)) {
40508
+ const rect = event.currentTarget.getBoundingClientRect();
40509
+ const side = event.clientX < rect.left + rect.width / 2 ? "left" : "right";
40510
+ return onSwimlaneDragOver(swimlane.id, side);
40511
+ }
39982
40512
  onDragOver(swimlane.id);
39983
40513
  if (dropIndex === null) {
39984
40514
  onDragOverIssue(swimlane.id, swimlane.issues.length);
@@ -39989,9 +40519,13 @@ var SwimlaneColumn = ({
39989
40519
  onDragLeave();
39990
40520
  }
39991
40521
  },
40522
+ "data-drop-side": dropSide ?? void 0,
39992
40523
  onDrop: (event) => {
39993
40524
  event.preventDefault();
39994
40525
  event.stopPropagation();
40526
+ if (isSwimlaneDrag(event.dataTransfer)) {
40527
+ return onDropSwimlane(event.dataTransfer.getData(SWIMLANE_DRAG_TYPE));
40528
+ }
39995
40529
  const issueId = event.dataTransfer.getData("text/plain");
39996
40530
  if (!issueId) return;
39997
40531
  const targetIndex = dropIndex ?? swimlane.issues.length;
@@ -39999,19 +40533,46 @@ var SwimlaneColumn = ({
39999
40533
  onDragLeave();
40000
40534
  },
40001
40535
  children: [
40002
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
40536
+ dropSide && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40537
+ "div",
40538
+ {
40539
+ "data-testid": "swimlane-drop-indicator",
40540
+ style: {
40541
+ position: "absolute",
40542
+ top: 8,
40543
+ bottom: 8,
40544
+ [dropSide]: -1,
40545
+ width: 2,
40546
+ borderRadius: 999,
40547
+ background: GUI_THEME.accent,
40548
+ boxShadow: `0 0 12px ${GUI_THEME.accent}`,
40549
+ zIndex: 2
40550
+ }
40551
+ }
40552
+ ),
40553
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
40003
40554
  "header",
40004
40555
  {
40556
+ draggable: !swimlane.readonly,
40557
+ "data-testid": "swimlane-handle",
40558
+ onDragStart: (event) => {
40559
+ event.stopPropagation();
40560
+ event.dataTransfer.effectAllowed = "move";
40561
+ event.dataTransfer.setData(SWIMLANE_DRAG_TYPE, swimlane.id);
40562
+ event.dataTransfer.setData("text/plain", "");
40563
+ },
40564
+ onDragEnd: onSwimlaneDragEnd,
40005
40565
  style: {
40006
40566
  height: 48,
40007
40567
  flexShrink: 0,
40008
40568
  display: "flex",
40009
40569
  fontSize: 12,
40010
40570
  justifyContent: "space-between",
40011
- alignItems: "center"
40571
+ alignItems: "center",
40572
+ cursor: swimlane.readonly ? "default" : "grab"
40012
40573
  },
40013
40574
  children: [
40014
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
40575
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(
40015
40576
  "div",
40016
40577
  {
40017
40578
  style: {
@@ -40020,50 +40581,71 @@ var SwimlaneColumn = ({
40020
40581
  gap: 8
40021
40582
  },
40022
40583
  children: [
40023
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
40584
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40024
40585
  "span",
40025
40586
  {
40026
40587
  style: { color: selected ? GUI_THEME.accent : GUI_THEME.secondary },
40027
40588
  children: selected ? "\u276F" : " "
40028
40589
  }
40029
40590
  ),
40030
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
40591
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40031
40592
  "strong",
40032
40593
  {
40033
40594
  style: { color: selected ? GUI_THEME.accent : GUI_THEME.primary },
40034
40595
  children: swimlane.title
40035
40596
  }
40036
40597
  ),
40037
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("span", { style: { color: GUI_THEME.dim }, children: [
40598
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("span", { style: { color: GUI_THEME.dim }, children: [
40038
40599
  "(",
40039
40600
  swimlane.issues.length,
40040
40601
  ")"
40041
40602
  ] }),
40042
- swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
40603
+ swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40043
40604
  "span",
40044
40605
  {
40045
40606
  title: "Read-only",
40046
40607
  style: { display: "flex", color: GUI_THEME.dim },
40047
- children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(IconLock, {})
40608
+ children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(IconLock, {})
40048
40609
  }
40049
40610
  )
40050
40611
  ]
40051
40612
  }
40052
40613
  ),
40053
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
40054
- Button,
40055
- {
40056
- variant: "ghost",
40057
- onClick: () => onCreateIssue(swimlane.id),
40058
- disabled: swimlane.readonly,
40059
- title: "Add issue",
40060
- children: "+"
40061
- }
40062
- ) })
40614
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 2 }, children: [
40615
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40616
+ Button,
40617
+ {
40618
+ variant: "ghost",
40619
+ onClick: () => onCreateIssue(swimlane.id),
40620
+ disabled: swimlane.readonly,
40621
+ title: "Add issue",
40622
+ children: "+"
40623
+ }
40624
+ ),
40625
+ !swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40626
+ KebabMenu,
40627
+ {
40628
+ testId: "swimlane-menu",
40629
+ items: [
40630
+ {
40631
+ id: "rename",
40632
+ label: "rename",
40633
+ onSelect: () => onRenameSwimlane(swimlane.id)
40634
+ },
40635
+ {
40636
+ id: "delete",
40637
+ label: "delete",
40638
+ danger: true,
40639
+ onSelect: () => onDeleteSwimlane(swimlane.id)
40640
+ }
40641
+ ]
40642
+ }
40643
+ )
40644
+ ] })
40063
40645
  ]
40064
40646
  }
40065
40647
  ),
40066
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
40648
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40067
40649
  "div",
40068
40650
  {
40069
40651
  style: {
@@ -40074,24 +40656,10 @@ var SwimlaneColumn = ({
40074
40656
  flex: 1,
40075
40657
  minHeight: 0
40076
40658
  },
40077
- children: swimlane.issues.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
40078
- dropIndex === 0 && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(DropIndicator, {}),
40079
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
40080
- "div",
40081
- {
40082
- style: {
40083
- padding: 24,
40084
- textAlign: "center",
40085
- color: GUI_THEME.dim,
40086
- fontSize: 12
40087
- },
40088
- children: "Drop issue here"
40089
- }
40090
- )
40091
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_jsx_runtime24.Fragment, { children: [
40092
- swimlane.issues.map((ticket, index2) => /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(import_react12.default.Fragment, { children: [
40093
- dropIndex === index2 && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(DropIndicator, {}),
40094
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
40659
+ children: swimlane.issues.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(import_jsx_runtime29.Fragment, {}) : /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_jsx_runtime29.Fragment, { children: [
40660
+ swimlane.issues.map((ticket, index2) => /* @__PURE__ */ (0, import_jsx_runtime29.jsxs)(import_react14.default.Fragment, { children: [
40661
+ dropIndex === index2 && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(DropIndicator, {}),
40662
+ /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
40095
40663
  TicketCard,
40096
40664
  {
40097
40665
  ticket,
@@ -40109,7 +40677,7 @@ var SwimlaneColumn = ({
40109
40677
  }
40110
40678
  )
40111
40679
  ] }, ticket.id)),
40112
- dropIndex === swimlane.issues.length && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(DropIndicator, {})
40680
+ dropIndex === swimlane.issues.length && /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(DropIndicator, {})
40113
40681
  ] })
40114
40682
  }
40115
40683
  )
@@ -40119,9 +40687,9 @@ var SwimlaneColumn = ({
40119
40687
  };
40120
40688
 
40121
40689
  // source/gui/client/components/GlobalScrollbarStyles.tsx
40122
- var import_jsx_runtime25 = __toESM(require_jsx_runtime(), 1);
40690
+ var import_jsx_runtime30 = __toESM(require_jsx_runtime(), 1);
40123
40691
  var SCROLLBAR_SIZE = 3;
40124
- var GlobalScrollbarStyles = () => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("style", { children: `
40692
+ var GlobalScrollbarStyles = () => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("style", { children: `
40125
40693
  /* Gated deliberately: Chrome implements scrollbar-width/-color too, and
40126
40694
  setting either makes it ignore the ::-webkit-scrollbar rules below. */
40127
40695
  @supports not selector(::-webkit-scrollbar) {
@@ -40162,8 +40730,8 @@ var GlobalScrollbarStyles = () => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
40162
40730
  ` });
40163
40731
 
40164
40732
  // source/gui/client/components/ErrorToast.tsx
40165
- var import_jsx_runtime26 = __toESM(require_jsx_runtime(), 1);
40166
- var ErrorToast = ({ message, onDismiss }) => /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
40733
+ var import_jsx_runtime31 = __toESM(require_jsx_runtime(), 1);
40734
+ var ErrorToast = ({ message, onDismiss }) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
40167
40735
  "div",
40168
40736
  {
40169
40737
  style: {
@@ -40184,8 +40752,8 @@ var ErrorToast = ({ message, onDismiss }) => /* @__PURE__ */ (0, import_jsx_runt
40184
40752
  boxShadow: "0 4px 16px rgba(0, 0, 0, 0.4)"
40185
40753
  },
40186
40754
  children: [
40187
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("span", { style: { flex: 1, minWidth: 0, overflowWrap: "anywhere" }, children: message }),
40188
- /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
40755
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { style: { flex: 1, minWidth: 0, overflowWrap: "anywhere" }, children: message }),
40756
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
40189
40757
  "button",
40190
40758
  {
40191
40759
  onClick: onDismiss,
@@ -40206,7 +40774,7 @@ var ErrorToast = ({ message, onDismiss }) => /* @__PURE__ */ (0, import_jsx_runt
40206
40774
  );
40207
40775
 
40208
40776
  // source/gui/client/components/TimeScrubber.tsx
40209
- var import_react15 = __toESM(require_react(), 1);
40777
+ var import_react17 = __toESM(require_react(), 1);
40210
40778
 
40211
40779
  // source/lib/utils/date.utils.ts
40212
40780
  var pad = (n) => String(n).padStart(2, "0");
@@ -40219,8 +40787,9 @@ var minOf = (values, seed) => values.reduce((min, value) => value < min ? value
40219
40787
  var maxOf = (values, seed) => values.reduce((max, value) => value > max ? value : max, seed);
40220
40788
 
40221
40789
  // source/gui/client/lib/scrubber.ts
40222
- var import_react13 = __toESM(require_react(), 1);
40790
+ var import_react15 = __toESM(require_react(), 1);
40223
40791
  var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
40792
+ var isLayoutMode = (value) => value === "even" || value === "real";
40224
40793
  var TRACK_HEIGHT = 24;
40225
40794
  var EVENTS_MODE_TOTAL_HEIGHT = 8 + TRACK_HEIGHT * 2;
40226
40795
  var EVENTS_SCATTER_HEIGHT = TRACK_HEIGHT + 16;
@@ -40241,12 +40810,11 @@ var bucketCountForSpan = (spanMs) => {
40241
40810
  );
40242
40811
  };
40243
40812
  var buildAxis = (timeline, commits, now = Date.now()) => {
40244
- const commitBounds = commits.length ? commits.map((c) => c.time) : [now];
40245
- const earliest = minOf(
40246
- commitBounds,
40247
- timeline?.buckets[0]?.t ?? timeline?.earliest ?? now
40248
- );
40249
- const latest = maxOf(commitBounds, timeline?.latest ?? now);
40813
+ const commitTimes = commits.map((c) => c.time);
40814
+ const windowStart = timeline?.earliest ?? minOf(commitTimes, now);
40815
+ const windowEnd = timeline?.latest ?? maxOf(commitTimes, now);
40816
+ const earliest = minOf(commitTimes, windowStart);
40817
+ const latest = maxOf(commitTimes, windowEnd);
40250
40818
  const span = Math.max(1, latest - earliest);
40251
40819
  const bucketCount = bucketCountForSpan(span);
40252
40820
  const bucketMs = span / bucketCount;
@@ -40308,6 +40876,7 @@ var CATEGORY_BY_ACTION = {
40308
40876
  "add.issue.assignee": "assigning",
40309
40877
  "remove.issue.assignee": "assigning",
40310
40878
  "create.contributor": "assigning",
40879
+ "rename.contributor": "assigning",
40311
40880
  "tombstone.contributor": "assigning",
40312
40881
  "restore.contributor": "assigning",
40313
40882
  "link.contributor.user": "assigning"
@@ -40353,6 +40922,7 @@ var buildEventDots = (timeline, view = "all", hiddenIds = /* @__PURE__ */ new Se
40353
40922
  {
40354
40923
  // Two events can share a millisecond, so time alone is not a key.
40355
40924
  key: `${entry.t}-${index2}`,
40925
+ id: entry.id,
40356
40926
  t: entry.t,
40357
40927
  count: null,
40358
40928
  label: entry.label,
@@ -40373,6 +40943,7 @@ var buildEventDots = (timeline, view = "all", hiddenIds = /* @__PURE__ */ new Se
40373
40943
  const intensity = bucket.count / maxCount;
40374
40944
  return {
40375
40945
  key: String(bucket.t),
40946
+ id: null,
40376
40947
  t: bucket.t,
40377
40948
  count: bucket.count,
40378
40949
  label: null,
@@ -40392,8 +40963,16 @@ var hourFractionForTime = (time) => {
40392
40963
  const date = new Date(time);
40393
40964
  return (date.getHours() * 60 + date.getMinutes()) / (24 * 60);
40394
40965
  };
40395
- var SEGMENT_UNIT_ORDER = ["hour", "day", "week", "month", "year"];
40966
+ var SEGMENT_UNIT_ORDER = [
40967
+ "minute",
40968
+ "hour",
40969
+ "day",
40970
+ "week",
40971
+ "month",
40972
+ "year"
40973
+ ];
40396
40974
  var APPROX_UNIT_MS = {
40975
+ minute: 60 * 1e3,
40397
40976
  hour: 60 * 60 * 1e3,
40398
40977
  day: 24 * 60 * 60 * 1e3,
40399
40978
  week: 7 * 24 * 60 * 60 * 1e3,
@@ -40401,9 +40980,16 @@ var APPROX_UNIT_MS = {
40401
40980
  year: 365.25 * 24 * 60 * 60 * 1e3
40402
40981
  };
40403
40982
  var MAX_SEGMENTS = 35;
40404
- var chooseSegmentUnit = (spanMs) => SEGMENT_UNIT_ORDER.find(
40405
- (unit) => spanMs / APPROX_UNIT_MS[unit] <= MAX_SEGMENTS
40406
- ) ?? "year";
40983
+ var MIN_SEGMENTS = 2;
40984
+ var chooseSegmentUnit = (spanMs) => {
40985
+ const index2 = SEGMENT_UNIT_ORDER.findIndex(
40986
+ (unit) => spanMs / APPROX_UNIT_MS[unit] <= MAX_SEGMENTS
40987
+ );
40988
+ if (index2 === -1) return "year";
40989
+ const chosen = SEGMENT_UNIT_ORDER[index2];
40990
+ if (spanMs / APPROX_UNIT_MS[chosen] >= MIN_SEGMENTS) return chosen;
40991
+ return SEGMENT_UNIT_ORDER[Math.max(0, index2 - 1)];
40992
+ };
40407
40993
  var WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
40408
40994
  var MONTH_LABELS = [
40409
40995
  "Jan",
@@ -40420,7 +41006,8 @@ var MONTH_LABELS = [
40420
41006
  "Dec"
40421
41007
  ];
40422
41008
  var advanceByUnit = (date, unit) => {
40423
- if (unit === "hour") date.setHours(date.getHours() + 1);
41009
+ if (unit === "minute") date.setMinutes(date.getMinutes() + 1);
41010
+ else if (unit === "hour") date.setHours(date.getHours() + 1);
40424
41011
  else if (unit === "day") date.setDate(date.getDate() + 1);
40425
41012
  else if (unit === "week") date.setDate(date.getDate() + 7);
40426
41013
  else if (unit === "month") date.setMonth(date.getMonth() + 1);
@@ -40428,7 +41015,8 @@ var advanceByUnit = (date, unit) => {
40428
41015
  };
40429
41016
  var segmentAt = (time, unit) => {
40430
41017
  const start2 = new Date(time);
40431
- if (unit === "hour") start2.setMinutes(0, 0, 0);
41018
+ if (unit === "minute") start2.setSeconds(0, 0);
41019
+ else if (unit === "hour") start2.setMinutes(0, 0, 0);
40432
41020
  else start2.setHours(0, 0, 0, 0);
40433
41021
  if (unit === "week") {
40434
41022
  start2.setDate(start2.getDate() - (start2.getDay() + 6) % 7);
@@ -40440,7 +41028,10 @@ var segmentAt = (time, unit) => {
40440
41028
  const end = new Date(start2);
40441
41029
  advanceByUnit(end, unit);
40442
41030
  const lastDay = new Date(end.getTime() - 1);
40443
- const label = unit === "hour" ? `${WEEKDAY_LABELS[start2.getDay()]} ${String(start2.getHours()).padStart(
41031
+ const clock = (date) => `${String(date.getHours()).padStart(2, "0")}:${String(
41032
+ date.getMinutes()
41033
+ ).padStart(2, "0")}`;
41034
+ const label = unit === "minute" ? clock(start2) : unit === "hour" ? `${WEEKDAY_LABELS[start2.getDay()]} ${String(start2.getHours()).padStart(
40444
41035
  2,
40445
41036
  "0"
40446
41037
  )}:00` : unit === "day" ? `${WEEKDAY_LABELS[start2.getDay()]} ${start2.getDate()} ${MONTH_LABELS[start2.getMonth()]}` : unit === "week" ? start2.getMonth() === lastDay.getMonth() ? `${start2.getDate()}\u2013${lastDay.getDate()} ${MONTH_LABELS[start2.getMonth()]}` : `${start2.getDate()} ${MONTH_LABELS[start2.getMonth()]} \u2013 ${lastDay.getDate()} ${MONTH_LABELS[lastDay.getMonth()]}` : unit === "month" ? `${MONTH_LABELS[start2.getMonth()]} ${start2.getFullYear()}` : `${start2.getFullYear()}`;
@@ -40452,14 +41043,23 @@ var formatInterval = (start2, end) => {
40452
41043
  const endLabel = isSameDay(startDate, endDate) ? formatTimeOfDay(endDate) : formatDateTime(endDate);
40453
41044
  return `${formatDateTime(startDate)} \u2013 ${endLabel}`;
40454
41045
  };
40455
- var SCOPES = ["day", "week", "month", "year", "all"];
41046
+ var SCOPES = [
41047
+ "hour",
41048
+ "day",
41049
+ "week",
41050
+ "month",
41051
+ "year",
41052
+ "all"
41053
+ ];
40456
41054
  var SCOPE_DURATION_MS = {
41055
+ hour: 60 * 60 * 1e3,
40457
41056
  day: 24 * 60 * 60 * 1e3,
40458
41057
  week: 7 * 24 * 60 * 60 * 1e3,
40459
41058
  month: 30 * 24 * 60 * 60 * 1e3,
40460
41059
  year: 365 * 24 * 60 * 60 * 1e3
40461
41060
  };
40462
41061
  var SCOPE_RECENT_LABELS = {
41062
+ hour: "Last hour",
40463
41063
  day: "Last 24 hours",
40464
41064
  week: "Last 7 days",
40465
41065
  month: "Last 30 days",
@@ -40542,10 +41142,10 @@ var SCRUBBER_KEYFRAMES = `
40542
41142
  `;
40543
41143
  var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
40544
41144
  var usePrefersReducedMotion = () => {
40545
- const [reduced, setReduced] = (0, import_react13.useState)(
41145
+ const [reduced, setReduced] = (0, import_react15.useState)(
40546
41146
  () => window.matchMedia(REDUCED_MOTION_QUERY).matches
40547
41147
  );
40548
- (0, import_react13.useEffect)(() => {
41148
+ (0, import_react15.useEffect)(() => {
40549
41149
  const query = window.matchMedia(REDUCED_MOTION_QUERY);
40550
41150
  const onChange = () => setReduced(query.matches);
40551
41151
  query.addEventListener("change", onChange);
@@ -40554,12 +41154,12 @@ var usePrefersReducedMotion = () => {
40554
41154
  return reduced;
40555
41155
  };
40556
41156
  var useExitTransition = (visible, durationMs) => {
40557
- const [mounted, setMounted] = (0, import_react13.useState)(visible);
40558
- const [leaving, setLeaving] = (0, import_react13.useState)(false);
40559
- const wasVisible = (0, import_react13.useRef)(visible);
40560
- const duration = (0, import_react13.useRef)(durationMs);
41157
+ const [mounted, setMounted] = (0, import_react15.useState)(visible);
41158
+ const [leaving, setLeaving] = (0, import_react15.useState)(false);
41159
+ const wasVisible = (0, import_react15.useRef)(visible);
41160
+ const duration = (0, import_react15.useRef)(durationMs);
40561
41161
  duration.current = durationMs;
40562
- (0, import_react13.useEffect)(() => {
41162
+ (0, import_react15.useEffect)(() => {
40563
41163
  if (visible === wasVisible.current) return;
40564
41164
  wasVisible.current = visible;
40565
41165
  if (visible) {
@@ -40578,10 +41178,10 @@ var useExitTransition = (visible, durationMs) => {
40578
41178
  }, duration.current);
40579
41179
  return () => clearTimeout(timeout);
40580
41180
  }, [visible]);
40581
- return { mounted, leaving };
41181
+ return (0, import_react15.useMemo)(() => ({ mounted, leaving }), [mounted, leaving]);
40582
41182
  };
40583
41183
  var usePersistedFlag = (key, fallback) => {
40584
- const [value, setValue] = (0, import_react13.useState)(() => {
41184
+ const [value, setValue] = (0, import_react15.useState)(() => {
40585
41185
  const stored = localStorage.getItem(key);
40586
41186
  return stored === null ? fallback : stored === "true";
40587
41187
  });
@@ -40608,10 +41208,10 @@ var issuePassesBoardFilter = (issue, commentAuthorIds, filter) => {
40608
41208
  };
40609
41209
 
40610
41210
  // source/gui/client/components/ScrubberParts.tsx
40611
- var import_react14 = __toESM(require_react(), 1);
41211
+ var import_react16 = __toESM(require_react(), 1);
40612
41212
 
40613
41213
  // source/gui/client/components/Checkbox.tsx
40614
- var import_jsx_runtime27 = __toESM(require_jsx_runtime(), 1);
41214
+ var import_jsx_runtime32 = __toESM(require_jsx_runtime(), 1);
40615
41215
  var Checkbox = ({
40616
41216
  label,
40617
41217
  checked,
@@ -40621,7 +41221,7 @@ var Checkbox = ({
40621
41221
  title
40622
41222
  }) => {
40623
41223
  const color2 = checked ? activeColor : GUI_THEME.dim;
40624
- return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
41224
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
40625
41225
  "label",
40626
41226
  {
40627
41227
  title,
@@ -40635,7 +41235,7 @@ var Checkbox = ({
40635
41235
  opacity: disabled ? 0.4 : 1
40636
41236
  },
40637
41237
  children: [
40638
- /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(
41238
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
40639
41239
  "span",
40640
41240
  {
40641
41241
  style: {
@@ -40652,7 +41252,7 @@ var Checkbox = ({
40652
41252
  flexShrink: 0
40653
41253
  },
40654
41254
  children: [
40655
- /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
41255
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40656
41256
  "input",
40657
41257
  {
40658
41258
  type: "checkbox",
@@ -40670,7 +41270,7 @@ var Checkbox = ({
40670
41270
  }
40671
41271
  }
40672
41272
  ),
40673
- checked && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
41273
+ checked && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
40674
41274
  "span",
40675
41275
  {
40676
41276
  style: {
@@ -40691,8 +41291,8 @@ var Checkbox = ({
40691
41291
  };
40692
41292
 
40693
41293
  // source/gui/client/components/IconBars.tsx
40694
- var import_jsx_runtime28 = __toESM(require_jsx_runtime(), 1);
40695
- var IconBars = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(
41294
+ var import_jsx_runtime33 = __toESM(require_jsx_runtime(), 1);
41295
+ var IconBars = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
40696
41296
  "svg",
40697
41297
  {
40698
41298
  width: size,
@@ -40704,18 +41304,18 @@ var IconBars = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)
40704
41304
  strokeLinecap: "round",
40705
41305
  "aria-hidden": "true",
40706
41306
  children: [
40707
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("line", { x1: "3", y1: "8.5", x2: "3", y2: "15.5" }),
40708
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("line", { x1: "7.5", y1: "4.5", x2: "7.5", y2: "19.5" }),
40709
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("line", { x1: "12", y1: "7", x2: "12", y2: "17" }),
40710
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("line", { x1: "16.5", y1: "3", x2: "16.5", y2: "21" }),
40711
- /* @__PURE__ */ (0, import_jsx_runtime28.jsx)("line", { x1: "21", y1: "9.5", x2: "21", y2: "14.5" })
41307
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("line", { x1: "3", y1: "8.5", x2: "3", y2: "15.5" }),
41308
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("line", { x1: "7.5", y1: "4.5", x2: "7.5", y2: "19.5" }),
41309
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("line", { x1: "12", y1: "7", x2: "12", y2: "17" }),
41310
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("line", { x1: "16.5", y1: "3", x2: "16.5", y2: "21" }),
41311
+ /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("line", { x1: "21", y1: "9.5", x2: "21", y2: "14.5" })
40712
41312
  ]
40713
41313
  }
40714
41314
  );
40715
41315
 
40716
41316
  // source/gui/client/components/IconChevronDown.tsx
40717
- var import_jsx_runtime29 = __toESM(require_jsx_runtime(), 1);
40718
- var IconChevronDown = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime29.jsx)(
41317
+ var import_jsx_runtime34 = __toESM(require_jsx_runtime(), 1);
41318
+ var IconChevronDown = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
40719
41319
  "svg",
40720
41320
  {
40721
41321
  width: size,
@@ -40727,13 +41327,13 @@ var IconChevronDown = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime2
40727
41327
  strokeLinecap: "round",
40728
41328
  strokeLinejoin: "round",
40729
41329
  "aria-hidden": "true",
40730
- children: /* @__PURE__ */ (0, import_jsx_runtime29.jsx)("polyline", { points: "6 9 12 15 18 9" })
41330
+ children: /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("polyline", { points: "6 9 12 15 18 9" })
40731
41331
  }
40732
41332
  );
40733
41333
 
40734
41334
  // source/gui/client/components/IconChevronRight.tsx
40735
- var import_jsx_runtime30 = __toESM(require_jsx_runtime(), 1);
40736
- var IconChevronRight = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime30.jsx)(
41335
+ var import_jsx_runtime35 = __toESM(require_jsx_runtime(), 1);
41336
+ var IconChevronRight = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
40737
41337
  "svg",
40738
41338
  {
40739
41339
  width: size,
@@ -40745,13 +41345,13 @@ var IconChevronRight = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime
40745
41345
  strokeLinecap: "round",
40746
41346
  strokeLinejoin: "round",
40747
41347
  "aria-hidden": "true",
40748
- children: /* @__PURE__ */ (0, import_jsx_runtime30.jsx)("polyline", { points: "9 18 15 12 9 6" })
41348
+ children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("polyline", { points: "9 18 15 12 9 6" })
40749
41349
  }
40750
41350
  );
40751
41351
 
40752
41352
  // source/gui/client/components/IconScatter.tsx
40753
- var import_jsx_runtime31 = __toESM(require_jsx_runtime(), 1);
40754
- var IconScatter = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
41353
+ var import_jsx_runtime36 = __toESM(require_jsx_runtime(), 1);
41354
+ var IconScatter = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
40755
41355
  "svg",
40756
41356
  {
40757
41357
  width: size,
@@ -40761,17 +41361,17 @@ var IconScatter = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime31.js
40761
41361
  stroke: "none",
40762
41362
  "aria-hidden": "true",
40763
41363
  children: [
40764
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("circle", { cx: "4.5", cy: "8", r: "2.2" }),
40765
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("circle", { cx: "9.5", cy: "17", r: "2.2" }),
40766
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("circle", { cx: "13", cy: "6.5", r: "2.2" }),
40767
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("circle", { cx: "17", cy: "14.5", r: "2.2" }),
40768
- /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("circle", { cx: "20.5", cy: "8.5", r: "2.2" })
41364
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("circle", { cx: "4.5", cy: "8", r: "2.2" }),
41365
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("circle", { cx: "9.5", cy: "17", r: "2.2" }),
41366
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("circle", { cx: "13", cy: "6.5", r: "2.2" }),
41367
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("circle", { cx: "17", cy: "14.5", r: "2.2" }),
41368
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("circle", { cx: "20.5", cy: "8.5", r: "2.2" })
40769
41369
  ]
40770
41370
  }
40771
41371
  );
40772
41372
 
40773
41373
  // source/gui/client/components/ScrubberParts.tsx
40774
- var import_jsx_runtime32 = __toESM(require_jsx_runtime(), 1);
41374
+ var import_jsx_runtime37 = __toESM(require_jsx_runtime(), 1);
40775
41375
  var toggleButtonStyle = (active) => ({
40776
41376
  background: "transparent",
40777
41377
  border: `1px solid ${active ? GUI_THEME.accent : GUI_THEME.dim}`,
@@ -40781,6 +41381,20 @@ var toggleButtonStyle = (active) => ({
40781
41381
  padding: "2px 8px",
40782
41382
  cursor: "pointer"
40783
41383
  });
41384
+ var scopeButtonStyle = (active) => ({
41385
+ background: "transparent",
41386
+ border: "none",
41387
+ borderBottom: `1px solid ${active ? GUI_THEME.accent : "transparent"}`,
41388
+ color: active ? GUI_THEME.primary : GUI_THEME.dim,
41389
+ borderRadius: 0,
41390
+ fontSize: 10,
41391
+ padding: "2px 6px 3px",
41392
+ cursor: "pointer"
41393
+ });
41394
+ var mutedStyle = {
41395
+ opacity: 0.4,
41396
+ cursor: "not-allowed"
41397
+ };
40784
41398
  var iconToggleButtonStyle = (active) => ({
40785
41399
  ...toggleButtonStyle(active),
40786
41400
  display: "inline-flex",
@@ -40873,8 +41487,8 @@ var onlyButtonStyle = {
40873
41487
  cursor: "pointer"
40874
41488
  };
40875
41489
  var useDismissOnOutsideClick = (open, onDismiss) => {
40876
- const ref = (0, import_react14.useRef)(null);
40877
- (0, import_react14.useEffect)(() => {
41490
+ const ref = (0, import_react16.useRef)(null);
41491
+ (0, import_react16.useEffect)(() => {
40878
41492
  if (!open) return;
40879
41493
  const onPointerDown = (event) => {
40880
41494
  if (!ref.current?.contains(event.target)) onDismiss();
@@ -40897,7 +41511,7 @@ var Radio = ({
40897
41511
  color: color2,
40898
41512
  disabled,
40899
41513
  onSelect
40900
- }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
41514
+ }) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
40901
41515
  "button",
40902
41516
  {
40903
41517
  type: "button",
@@ -40919,7 +41533,7 @@ var Radio = ({
40919
41533
  opacity: disabled ? 0.4 : 1
40920
41534
  },
40921
41535
  children: [
40922
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41536
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
40923
41537
  "span",
40924
41538
  {
40925
41539
  style: {
@@ -40932,7 +41546,7 @@ var Radio = ({
40932
41546
  justifyContent: "center",
40933
41547
  flexShrink: 0
40934
41548
  },
40935
- children: selected && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41549
+ children: selected && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
40936
41550
  "span",
40937
41551
  {
40938
41552
  style: {
@@ -40950,6 +41564,7 @@ var Radio = ({
40950
41564
  }
40951
41565
  );
40952
41566
  var BoardSeriesGroup = ({
41567
+ connected,
40953
41568
  showIssues,
40954
41569
  view,
40955
41570
  identities,
@@ -40969,35 +41584,39 @@ var BoardSeriesGroup = ({
40969
41584
  const label = sole ? `${VIEW_LABELS[view]}: ${sole.name}` : partial ? `${VIEW_LABELS[view]} (multi)` : VIEW_LABELS[view];
40970
41585
  const color2 = sole?.color ?? (partial ? GUI_THEME.dim2 : boardViewColor(view));
40971
41586
  const ref = useDismissOnOutsideClick(expanded, onToggleExpanded);
40972
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
41587
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
40973
41588
  "div",
40974
41589
  {
40975
41590
  ref,
40976
41591
  style: { position: "relative", display: "flex", flexDirection: "column" },
40977
41592
  children: [
40978
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
40979
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41593
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
41594
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
40980
41595
  Checkbox,
40981
41596
  {
40982
41597
  label: null,
40983
41598
  title: "Show board events",
40984
41599
  checked: showIssues,
40985
41600
  activeColor: color2,
41601
+ disabled: !connected,
40986
41602
  onChange: onChangeShowIssues
40987
41603
  }
40988
41604
  ),
40989
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
41605
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
40990
41606
  "button",
40991
41607
  {
40992
41608
  type: "button",
40993
41609
  onClick: onToggleExpanded,
40994
- disabled: !showIssues,
41610
+ disabled: !showIssues || !connected,
40995
41611
  title: filtered ? "Choose what the board series plots" : "This window holds too many events to split by kind",
40996
41612
  "aria-haspopup": "listbox",
40997
41613
  "aria-expanded": expanded,
40998
- style: selectTriggerStyle(color2, !showIssues),
41614
+ style: {
41615
+ ...selectTriggerStyle(color2, !showIssues),
41616
+ ...connected ? {} : mutedStyle
41617
+ },
40999
41618
  children: [
41000
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41619
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41001
41620
  "span",
41002
41621
  {
41003
41622
  style: {
@@ -41008,22 +41627,22 @@ var BoardSeriesGroup = ({
41008
41627
  children: label
41009
41628
  }
41010
41629
  ),
41011
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { style: { display: "inline-flex", flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconChevronDown, { size: 12 }) })
41630
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("span", { style: { display: "inline-flex", flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(IconChevronDown, { size: 12 }) })
41012
41631
  ]
41013
41632
  }
41014
41633
  )
41015
41634
  ] }),
41016
- expanded && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { role: "radiogroup", style: popoverStyle, children: BOARD_VIEWS.map((option) => {
41635
+ expanded && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { role: "radiogroup", style: popoverStyle, children: BOARD_VIEWS.map((option) => {
41017
41636
  const selected = view === option;
41018
41637
  const hasList = identityAxisFor(option) !== null;
41019
41638
  const open = selected && identitiesExpanded;
41020
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
41639
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
41021
41640
  "div",
41022
41641
  {
41023
41642
  style: { display: "flex", flexDirection: "column", gap: 7 },
41024
41643
  children: [
41025
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 3 }, children: [
41026
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41644
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 3 }, children: [
41645
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41027
41646
  Radio,
41028
41647
  {
41029
41648
  label: VIEW_LABELS[option],
@@ -41033,7 +41652,7 @@ var BoardSeriesGroup = ({
41033
41652
  onSelect: () => onChangeView(option)
41034
41653
  }
41035
41654
  ),
41036
- hasList && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41655
+ hasList && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41037
41656
  "button",
41038
41657
  {
41039
41658
  type: "button",
@@ -41045,11 +41664,11 @@ var BoardSeriesGroup = ({
41045
41664
  title: open ? "Hide the list" : "Pick which to show",
41046
41665
  "aria-expanded": open,
41047
41666
  style: disclosureStyle,
41048
- children: open ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconChevronDown, { size: 12 }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconChevronRight, { size: 12 })
41667
+ children: open ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(IconChevronDown, { size: 12 }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(IconChevronRight, { size: 12 })
41049
41668
  }
41050
41669
  )
41051
41670
  ] }),
41052
- open && identities.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41671
+ open && identities.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41053
41672
  "div",
41054
41673
  {
41055
41674
  style: {
@@ -41059,7 +41678,7 @@ var BoardSeriesGroup = ({
41059
41678
  maxHeight: 132,
41060
41679
  overflowY: "auto"
41061
41680
  },
41062
- children: identities.map((identity) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
41681
+ children: identities.map((identity) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
41063
41682
  "div",
41064
41683
  {
41065
41684
  style: {
@@ -41069,7 +41688,7 @@ var BoardSeriesGroup = ({
41069
41688
  gap: 10
41070
41689
  },
41071
41690
  children: [
41072
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41691
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41073
41692
  Checkbox,
41074
41693
  {
41075
41694
  label: identity.name,
@@ -41079,7 +41698,7 @@ var BoardSeriesGroup = ({
41079
41698
  onChange: (next) => onToggleIdentity(identity.id, next)
41080
41699
  }
41081
41700
  ),
41082
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41701
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41083
41702
  "button",
41084
41703
  {
41085
41704
  type: "button",
@@ -41116,6 +41735,7 @@ var navButtonStyle = {
41116
41735
  lineHeight: 1
41117
41736
  };
41118
41737
  var ScrubberControls = ({
41738
+ connected,
41119
41739
  scope,
41120
41740
  offset,
41121
41741
  periodRange,
@@ -41142,7 +41762,7 @@ var ScrubberControls = ({
41142
41762
  onOnlyIdentity,
41143
41763
  onToggleCategoriesExpanded,
41144
41764
  onSetIdentitiesExpanded
41145
- }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
41765
+ }) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
41146
41766
  "div",
41147
41767
  {
41148
41768
  style: {
@@ -41152,18 +41772,19 @@ var ScrubberControls = ({
41152
41772
  gap: 12
41153
41773
  },
41154
41774
  children: [
41155
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
41156
- scope !== "all" && /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 2 }, children: [
41157
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41775
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
41776
+ scope !== "all" && /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 2 }, children: [
41777
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41158
41778
  "button",
41159
41779
  {
41160
41780
  title: "Earlier",
41781
+ disabled: !connected,
41161
41782
  onClick: () => onChangeOffset(offset + 1),
41162
- style: navButtonStyle,
41783
+ style: { ...navButtonStyle, ...connected ? {} : mutedStyle },
41163
41784
  children: "\u25C0"
41164
41785
  }
41165
41786
  ),
41166
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41787
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41167
41788
  "span",
41168
41789
  {
41169
41790
  style: {
@@ -41180,11 +41801,11 @@ var ScrubberControls = ({
41180
41801
  children: formatPeriodLabel(scope, offset, periodRange)
41181
41802
  }
41182
41803
  ),
41183
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41804
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41184
41805
  "button",
41185
41806
  {
41186
41807
  title: "Later",
41187
- disabled: offset === 0,
41808
+ disabled: offset === 0 || !connected,
41188
41809
  onClick: () => onChangeOffset(Math.max(0, offset - 1)),
41189
41810
  style: {
41190
41811
  ...navButtonStyle,
@@ -41195,53 +41816,68 @@ var ScrubberControls = ({
41195
41816
  }
41196
41817
  )
41197
41818
  ] }),
41198
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { style: { display: "flex", gap: 2 }, children: SCOPES.map((option) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41819
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { style: { display: "flex", gap: 2 }, children: SCOPES.map((option) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41199
41820
  "button",
41200
41821
  {
41822
+ "aria-pressed": scope === option,
41823
+ disabled: !connected,
41201
41824
  onClick: () => onChangeScope(option),
41202
- style: toggleButtonStyle(scope === option),
41825
+ style: {
41826
+ ...scopeButtonStyle(scope === option),
41827
+ ...connected ? {} : mutedStyle
41828
+ },
41203
41829
  children: scopeButtonLabel(option)
41204
41830
  },
41205
41831
  option
41206
41832
  )) })
41207
41833
  ] }),
41208
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { display: "flex", gap: 2 }, children: [
41209
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41834
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { style: { display: "flex", gap: 2 }, children: [
41835
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41210
41836
  "button",
41211
41837
  {
41212
41838
  title: "Volume \u2014 how much happened, per equal-width period, with no empty gaps for quiet stretches",
41213
41839
  "aria-label": "Volume",
41214
41840
  "aria-pressed": layoutMode === "even",
41841
+ disabled: !connected,
41215
41842
  onClick: () => onChangeLayoutMode("even"),
41216
- style: iconToggleButtonStyle(layoutMode === "even"),
41217
- children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconBars, { size: 13 })
41843
+ style: {
41844
+ ...iconToggleButtonStyle(layoutMode === "even"),
41845
+ ...connected ? {} : mutedStyle
41846
+ },
41847
+ children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(IconBars, { size: 13 })
41218
41848
  }
41219
41849
  ),
41220
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41850
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41221
41851
  "button",
41222
41852
  {
41223
41853
  title: "Events \u2014 individual events by exact moment, x is elapsed time and y is time of day",
41224
41854
  "aria-label": "Events",
41225
41855
  "aria-pressed": layoutMode === "real",
41856
+ disabled: !connected,
41226
41857
  onClick: () => onChangeLayoutMode("real"),
41227
- style: iconToggleButtonStyle(layoutMode === "real"),
41228
- children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconScatter, { size: 13 })
41858
+ style: {
41859
+ ...iconToggleButtonStyle(layoutMode === "real"),
41860
+ ...connected ? {} : mutedStyle
41861
+ },
41862
+ children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(IconScatter, { size: 13 })
41229
41863
  }
41230
41864
  )
41231
41865
  ] }),
41232
- /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("div", { style: { display: "flex", gap: 12, alignItems: "center" }, children: [
41233
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41866
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)("div", { style: { display: "flex", gap: 12, alignItems: "center" }, children: [
41867
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41234
41868
  Checkbox,
41235
41869
  {
41236
41870
  label: "Code",
41237
41871
  checked: showCommits,
41238
41872
  activeColor: GUI_THEME.green,
41873
+ disabled: !connected,
41239
41874
  onChange: onChangeShowCommits
41240
41875
  }
41241
41876
  ),
41242
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41877
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41243
41878
  BoardSeriesGroup,
41244
41879
  {
41880
+ connected,
41245
41881
  showIssues,
41246
41882
  view: boardView,
41247
41883
  identities,
@@ -41258,7 +41894,7 @@ var ScrubberControls = ({
41258
41894
  }
41259
41895
  )
41260
41896
  ] }),
41261
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41897
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41262
41898
  "button",
41263
41899
  {
41264
41900
  onClick: onReturnToLive,
@@ -41289,7 +41925,7 @@ var ScrubberControls = ({
41289
41925
  var ScrubberHeader = ({
41290
41926
  collapsed,
41291
41927
  onToggleCollapsed
41292
- }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41928
+ }) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41293
41929
  "div",
41294
41930
  {
41295
41931
  style: {
@@ -41299,7 +41935,7 @@ var ScrubberHeader = ({
41299
41935
  fontSize: 11,
41300
41936
  whiteSpace: "nowrap"
41301
41937
  },
41302
- children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41938
+ children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41303
41939
  "button",
41304
41940
  {
41305
41941
  onClick: onToggleCollapsed,
@@ -41316,7 +41952,7 @@ var ScrubberHeader = ({
41316
41952
  display: "inline-flex",
41317
41953
  alignItems: "center"
41318
41954
  },
41319
- children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconChevronRight, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(IconChevronDown, { size: 14 })
41955
+ children: collapsed ? /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(IconChevronRight, { size: 14 }) : /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(IconChevronDown, { size: 14 })
41320
41956
  }
41321
41957
  )
41322
41958
  }
@@ -41326,7 +41962,7 @@ var SegmentHighlight = ({
41326
41962
  fractionForTime
41327
41963
  }) => {
41328
41964
  const left = fractionForTime(segment.start);
41329
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41965
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41330
41966
  "div",
41331
41967
  {
41332
41968
  style: {
@@ -41358,7 +41994,7 @@ var SegmentHighlight = ({
41358
41994
  var TrackBaseline = ({
41359
41995
  color: color2,
41360
41996
  anchor
41361
- }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41997
+ }) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41362
41998
  "div",
41363
41999
  {
41364
42000
  style: {
@@ -41374,11 +42010,11 @@ var TrackBaseline = ({
41374
42010
  }
41375
42011
  }
41376
42012
  );
41377
- var HourAxisLabels = () => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_jsx_runtime32.Fragment, { children: [
42013
+ var HourAxisLabels = () => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_jsx_runtime37.Fragment, { children: [
41378
42014
  ["00:00", { top: EVENTS_MODE_VERTICAL_PADDING }],
41379
42015
  ["12:00", { top: "50%", transform: "translateY(-50%)" }],
41380
42016
  ["24:00", { bottom: EVENTS_MODE_VERTICAL_PADDING }]
41381
- ].map(([label, position3]) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42017
+ ].map(([label, position3]) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41382
42018
  "span",
41383
42019
  {
41384
42020
  style: {
@@ -41396,7 +42032,7 @@ var HourAxisLabels = () => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(import_
41396
42032
  var SeriesLayer = ({
41397
42033
  animate,
41398
42034
  children
41399
- }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42035
+ }) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41400
42036
  "div",
41401
42037
  {
41402
42038
  style: {
@@ -41412,12 +42048,30 @@ var SeriesLayer = ({
41412
42048
  children
41413
42049
  }
41414
42050
  );
41415
- var VolumeBars = ({
42051
+ var BucketHighlight = ({
42052
+ index: index2,
42053
+ bucketCount
42054
+ }) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
42055
+ "div",
42056
+ {
42057
+ style: {
42058
+ position: "absolute",
42059
+ left: `${index2 * (100 / bucketCount)}%`,
42060
+ top: 0,
42061
+ bottom: 0,
42062
+ width: `${100 / bucketCount}%`,
42063
+ // A bucket can be thinner than a pixel at wide spans.
42064
+ minWidth: 2,
42065
+ background: BUCKET_HIGHLIGHT_COLOR,
42066
+ pointerEvents: "none"
42067
+ }
42068
+ }
42069
+ );
42070
+ var VolumeBarsImpl = ({
41416
42071
  bars,
41417
42072
  bucketCount,
41418
42073
  firstBar,
41419
42074
  lastBar,
41420
- highlightedIndex,
41421
42075
  color: color2,
41422
42076
  direction,
41423
42077
  animate
@@ -41425,57 +42079,40 @@ var VolumeBars = ({
41425
42079
  const widthPercent = 100 / bucketCount;
41426
42080
  const barWidth = barWidthCss(bucketCount);
41427
42081
  const growsUp = direction === "up";
41428
- const [fresh, setFresh] = (0, import_react14.useState)(true);
41429
- (0, import_react14.useEffect)(() => {
42082
+ const [fresh, setFresh] = (0, import_react16.useState)(true);
42083
+ (0, import_react16.useEffect)(() => {
41430
42084
  const timeout = setTimeout(() => setFresh(false), BAR_ENTRANCE_TOTAL_MS);
41431
42085
  return () => clearTimeout(timeout);
41432
42086
  }, []);
41433
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
41434
- highlightedIndex !== null && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41435
- "div",
41436
- {
41437
- style: {
41438
- position: "absolute",
41439
- left: `${highlightedIndex * widthPercent}%`,
41440
- top: 0,
41441
- bottom: 0,
41442
- width: `${widthPercent}%`,
41443
- // A bucket can be thinner than a pixel at wide spans.
41444
- minWidth: 2,
41445
- background: BUCKET_HIGHLIGHT_COLOR,
41446
- pointerEvents: "none"
41447
- }
42087
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(import_jsx_runtime37.Fragment, { children: bars.map(({ index: index2, intensity }) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
42088
+ "div",
42089
+ {
42090
+ style: {
42091
+ position: "absolute",
42092
+ left: `${index2 * widthPercent}%`,
42093
+ ...growsUp ? { bottom: 0 } : { top: 0 },
42094
+ width: barWidth,
42095
+ height: 3 + intensity * (TRACK_HEIGHT - 3),
42096
+ borderRadius: growsUp ? "1px 1px 0 0" : "0 0 1px 1px",
42097
+ background: color2,
42098
+ opacity: 0.35 + intensity * 0.65,
42099
+ transformOrigin: growsUp ? "bottom" : "top",
42100
+ animation: animate && fresh ? barGrowAnimation(index2, firstBar, lastBar) : void 0,
42101
+ pointerEvents: "none"
41448
42102
  }
41449
- ),
41450
- bars.map(({ index: index2, intensity }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
41451
- "div",
41452
- {
41453
- style: {
41454
- position: "absolute",
41455
- left: `${index2 * widthPercent}%`,
41456
- ...growsUp ? { bottom: 0 } : { top: 0 },
41457
- width: barWidth,
41458
- height: 3 + intensity * (TRACK_HEIGHT - 3),
41459
- borderRadius: growsUp ? "1px 1px 0 0" : "0 0 1px 1px",
41460
- background: color2,
41461
- opacity: 0.35 + intensity * 0.65,
41462
- transformOrigin: growsUp ? "bottom" : "top",
41463
- animation: animate && fresh ? barGrowAnimation(index2, firstBar, lastBar) : void 0,
41464
- pointerEvents: "none"
41465
- }
41466
- },
41467
- index2
41468
- ))
41469
- ] });
42103
+ },
42104
+ index2
42105
+ )) });
41470
42106
  };
42107
+ var VolumeBars = (0, import_react16.memo)(VolumeBarsImpl);
41471
42108
  var ScrubberNeedle = ({ fraction }) => {
41472
- const [hovered, setHovered] = (0, import_react14.useState)(false);
42109
+ const [hovered, setHovered] = (0, import_react16.useState)(false);
41473
42110
  const hover = {
41474
42111
  onMouseEnter: () => setHovered(true),
41475
42112
  onMouseLeave: () => setHovered(false)
41476
42113
  };
41477
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(import_jsx_runtime32.Fragment, { children: [
41478
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42114
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_jsx_runtime37.Fragment, { children: [
42115
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41479
42116
  "div",
41480
42117
  {
41481
42118
  ...hover,
@@ -41495,7 +42132,7 @@ var ScrubberNeedle = ({ fraction }) => {
41495
42132
  }
41496
42133
  }
41497
42134
  ),
41498
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42135
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41499
42136
  "div",
41500
42137
  {
41501
42138
  ...hover,
@@ -41525,7 +42162,7 @@ var ScrubberHoverHint = ({
41525
42162
  fraction,
41526
42163
  trackWidthPx,
41527
42164
  empty: empty3 = false
41528
- }) => /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
42165
+ }) => /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
41529
42166
  "div",
41530
42167
  {
41531
42168
  style: {
@@ -41557,8 +42194,8 @@ var ScrubberHoverHint = ({
41557
42194
  zIndex: 5
41558
42195
  },
41559
42196
  children: [
41560
- segmentLabel && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { style: { fontSize: 10, color: GUI_THEME.dim }, children: segmentLabel }),
41561
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42197
+ segmentLabel && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)("div", { style: { fontSize: 10, color: GUI_THEME.dim }, children: segmentLabel }),
42198
+ /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41562
42199
  "div",
41563
42200
  {
41564
42201
  style: {
@@ -41571,7 +42208,7 @@ var ScrubberHoverHint = ({
41571
42208
  children: label
41572
42209
  }
41573
42210
  ),
41574
- rows.map((row, index2) => /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42211
+ rows.map((row, index2) => /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41575
42212
  "div",
41576
42213
  {
41577
42214
  style: {
@@ -41605,22 +42242,31 @@ var pointAt = (layers, x, y, width) => {
41605
42242
  }
41606
42243
  return best;
41607
42244
  };
42245
+ var DIMMED_ALPHA = 0.08;
42246
+ var HIGHLIGHT_RADIUS_SCALE = 2.2;
41608
42247
  var ScatterCanvas = ({
41609
42248
  layers,
41610
42249
  animate,
42250
+ highlightId,
41611
42251
  onPointEnter,
41612
42252
  onPointLeave,
41613
42253
  onInspectCommit
41614
42254
  }) => {
41615
- const canvasRef = (0, import_react14.useRef)(null);
41616
- const sizeRef = (0, import_react14.useRef)({ width: 0, height: 0 });
41617
- const layersRef = (0, import_react14.useRef)(layers);
42255
+ const canvasRef = (0, import_react16.useRef)(null);
42256
+ const sizeRef = (0, import_react16.useRef)({ width: 0, height: 0 });
42257
+ const layersRef = (0, import_react16.useRef)(layers);
41618
42258
  layersRef.current = layers;
41619
- const phasesRef = (0, import_react14.useRef)(/* @__PURE__ */ new Map());
41620
- const frameRef = (0, import_react14.useRef)(null);
41621
- const hoveredRef = (0, import_react14.useRef)(null);
41622
- const [entrancePlaying, setEntrancePlaying] = (0, import_react14.useState)(false);
41623
- const paint = (0, import_react14.useCallback)((now) => {
42259
+ const activeHighlight = (0, import_react16.useMemo)(
42260
+ () => highlightId !== null && layers.some((layer) => layer.points.some((point4) => point4.id === highlightId)) ? highlightId : null,
42261
+ [layers, highlightId]
42262
+ );
42263
+ const highlightRef = (0, import_react16.useRef)(activeHighlight);
42264
+ highlightRef.current = activeHighlight;
42265
+ const phasesRef = (0, import_react16.useRef)(/* @__PURE__ */ new Map());
42266
+ const frameRef = (0, import_react16.useRef)(null);
42267
+ const hoveredRef = (0, import_react16.useRef)(null);
42268
+ const [entrancePlaying, setEntrancePlaying] = (0, import_react16.useState)(false);
42269
+ const paint = (0, import_react16.useCallback)((now) => {
41624
42270
  const canvas = canvasRef.current;
41625
42271
  const context = canvas?.getContext("2d");
41626
42272
  if (!canvas || !context) return false;
@@ -41636,23 +42282,28 @@ var ScatterCanvas = ({
41636
42282
  for (const point4 of layer.points) {
41637
42283
  const scale = phase === null || phase === void 0 || done ? 1 : phase.mode === "in" ? dotEntranceScale(point4.key, elapsed) : dotExitScale(point4.key, elapsed);
41638
42284
  if (scale <= 0) continue;
41639
- context.globalAlpha = point4.opacity;
42285
+ const highlight = highlightRef.current;
42286
+ const isHighlighted = highlight !== null && point4.id === highlight;
42287
+ const radiusScale = scale * (isHighlighted ? HIGHLIGHT_RADIUS_SCALE : 1);
42288
+ const cx = point4.fraction * width;
42289
+ const cy = EVENTS_MODE_VERTICAL_PADDING + point4.hourFraction * EVENTS_SCATTER_HEIGHT;
42290
+ context.globalAlpha = highlight === null ? point4.opacity : isHighlighted ? 1 : DIMMED_ALPHA;
41640
42291
  context.fillStyle = point4.color;
41641
42292
  context.beginPath();
41642
- context.arc(
41643
- point4.fraction * width,
41644
- EVENTS_MODE_VERTICAL_PADDING + point4.hourFraction * EVENTS_SCATTER_HEIGHT,
41645
- point4.radius * scale,
41646
- 0,
41647
- Math.PI * 2
41648
- );
42293
+ context.arc(cx, cy, point4.radius * radiusScale, 0, Math.PI * 2);
41649
42294
  context.fill();
42295
+ if (isHighlighted) {
42296
+ context.globalAlpha = 0.25;
42297
+ context.beginPath();
42298
+ context.arc(cx, cy, point4.radius * radiusScale * 2.4, 0, Math.PI * 2);
42299
+ context.fill();
42300
+ }
41650
42301
  }
41651
42302
  }
41652
42303
  context.globalAlpha = 1;
41653
42304
  return running;
41654
42305
  }, []);
41655
- const run = (0, import_react14.useCallback)(() => {
42306
+ const run = (0, import_react16.useCallback)(() => {
41656
42307
  if (frameRef.current !== null) return;
41657
42308
  const step = () => {
41658
42309
  frameRef.current = null;
@@ -41662,7 +42313,7 @@ var ScatterCanvas = ({
41662
42313
  };
41663
42314
  frameRef.current = requestAnimationFrame(step);
41664
42315
  }, [paint]);
41665
- (0, import_react14.useEffect)(() => {
42316
+ (0, import_react16.useEffect)(() => {
41666
42317
  const canvas = canvasRef.current;
41667
42318
  const parent = canvas?.parentElement;
41668
42319
  if (!canvas || !parent) return;
@@ -41683,7 +42334,7 @@ var ScatterCanvas = ({
41683
42334
  return () => observer.disconnect();
41684
42335
  }, [paint]);
41685
42336
  const signature = layers.map((layer) => `${layer.id}:${layer.generation}:${layer.leaving}`).join("|");
41686
- (0, import_react14.useEffect)(() => {
42337
+ (0, import_react16.useEffect)(() => {
41687
42338
  const seen = /* @__PURE__ */ new Set();
41688
42339
  for (const layer of layers) {
41689
42340
  seen.add(layer.id);
@@ -41708,10 +42359,10 @@ var ScatterCanvas = ({
41708
42359
  if (animate) run();
41709
42360
  else paint(performance.now());
41710
42361
  }, [signature, animate, run, paint]);
41711
- (0, import_react14.useEffect)(() => {
42362
+ (0, import_react16.useEffect)(() => {
41712
42363
  if (frameRef.current === null) paint(performance.now());
41713
- }, [layers, paint]);
41714
- (0, import_react14.useEffect)(
42364
+ }, [layers, activeHighlight, paint]);
42365
+ (0, import_react16.useEffect)(
41715
42366
  () => () => {
41716
42367
  if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
41717
42368
  },
@@ -41726,11 +42377,13 @@ var ScatterCanvas = ({
41726
42377
  rect.width
41727
42378
  );
41728
42379
  };
41729
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
42380
+ return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
41730
42381
  "canvas",
41731
42382
  {
41732
42383
  ref: canvasRef,
42384
+ "data-testid": "scatter-canvas",
41733
42385
  "data-entrance": entrancePlaying ? "playing" : "done",
42386
+ "data-highlight": activeHighlight ?? "",
41734
42387
  style: { position: "absolute", inset: 0 },
41735
42388
  onMouseMove: (event) => {
41736
42389
  const point4 = hitTest(event);
@@ -41755,7 +42408,8 @@ var ScatterCanvas = ({
41755
42408
  };
41756
42409
 
41757
42410
  // source/gui/client/components/ScrubberLayout.tsx
41758
- var import_jsx_runtime33 = __toESM(require_jsx_runtime(), 1);
42411
+ var import_jsx_runtime38 = __toESM(require_jsx_runtime(), 1);
42412
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
41759
42413
  var ScrubberLayout = ({
41760
42414
  collapsed,
41761
42415
  onToggleCollapsed,
@@ -41763,7 +42417,7 @@ var ScrubberLayout = ({
41763
42417
  chart
41764
42418
  }) => {
41765
42419
  const { axis, layoutMode, animate, windowKey, on } = chart;
41766
- return /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
42420
+ return /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
41767
42421
  Panel,
41768
42422
  {
41769
42423
  as: "div",
@@ -41780,8 +42434,8 @@ var ScrubberLayout = ({
41780
42434
  overflow: "visible"
41781
42435
  },
41782
42436
  children: [
41783
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)("style", { children: SCRUBBER_KEYFRAMES }),
41784
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
42437
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)("style", { children: SCRUBBER_KEYFRAMES }),
42438
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
41785
42439
  "div",
41786
42440
  {
41787
42441
  style: {
@@ -41790,7 +42444,7 @@ var ScrubberLayout = ({
41790
42444
  gap: collapsed ? 0 : 8
41791
42445
  },
41792
42446
  children: [
41793
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
42447
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
41794
42448
  "div",
41795
42449
  {
41796
42450
  style: {
@@ -41802,14 +42456,14 @@ var ScrubberLayout = ({
41802
42456
  minHeight: 22
41803
42457
  },
41804
42458
  children: [
41805
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42459
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
41806
42460
  ScrubberHeader,
41807
42461
  {
41808
42462
  collapsed,
41809
42463
  onToggleCollapsed
41810
42464
  }
41811
42465
  ),
41812
- !collapsed && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(ScrubberControls, { ...controls })
42466
+ !collapsed && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(ScrubberControls, { ...controls })
41813
42467
  ]
41814
42468
  }
41815
42469
  ),
@@ -41817,9 +42471,10 @@ var ScrubberLayout = ({
41817
42471
  // spanning them and the gap between. Pointer handlers belong here
41818
42472
  // rather than on either chart, so a drag or hover anywhere across
41819
42473
  // the pair — the gap included — counts as one timeline.
41820
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
42474
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
41821
42475
  "div",
41822
42476
  {
42477
+ "data-testid": "scrubber-track",
41823
42478
  onPointerDown: on.onPointerDown,
41824
42479
  onPointerMove: on.onPointerMove,
41825
42480
  onPointerUp: on.onPointerEnd,
@@ -41831,17 +42486,17 @@ var ScrubberLayout = ({
41831
42486
  display: "flex",
41832
42487
  flexDirection: "column",
41833
42488
  gap: 8,
41834
- cursor: "pointer"
42489
+ cursor: chart.connected ? "pointer" : "default"
41835
42490
  },
41836
42491
  children: [
41837
- chart.hoveredSegment && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42492
+ chart.hoveredSegment && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
41838
42493
  SegmentHighlight,
41839
42494
  {
41840
42495
  segment: chart.hoveredSegment,
41841
42496
  fractionForTime: axis.fractionForTime
41842
42497
  }
41843
42498
  ),
41844
- /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
42499
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
41845
42500
  "div",
41846
42501
  {
41847
42502
  ref: chart.trackRef,
@@ -41856,32 +42511,41 @@ var ScrubberLayout = ({
41856
42511
  alignItems: "center"
41857
42512
  },
41858
42513
  children: [
41859
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42514
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
41860
42515
  TrackBaseline,
41861
42516
  {
41862
42517
  color: chart.issueSeriesColor,
41863
42518
  anchor: layoutMode === "even" ? "bottom" : "centre"
41864
42519
  }
41865
42520
  ),
41866
- layoutMode === "real" && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(HourAxisLabels, {}),
41867
- chart.showIssues && layoutMode === "even" && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(SeriesLayer, { animate, children: /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
41868
- VolumeBars,
41869
- {
41870
- bars: chart.issueBars,
41871
- bucketCount: axis.bucketCount,
41872
- firstBar: chart.issueBarRange[0],
41873
- lastBar: chart.issueBarRange[1],
41874
- highlightedIndex: chart.hoveredBucketIndex,
41875
- color: chart.issueSeriesColor,
41876
- direction: "up",
41877
- animate
41878
- }
41879
- ) }, `issues-${windowKey}`),
41880
- layoutMode === "real" && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42521
+ layoutMode === "real" && axis.span >= DAY_MS2 && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(HourAxisLabels, {}),
42522
+ chart.showIssues && layoutMode === "even" && /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(import_jsx_runtime38.Fragment, { children: [
42523
+ chart.hoveredBucketIndex !== null && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
42524
+ BucketHighlight,
42525
+ {
42526
+ index: chart.hoveredBucketIndex,
42527
+ bucketCount: axis.bucketCount
42528
+ }
42529
+ ),
42530
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(SeriesLayer, { animate, children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
42531
+ VolumeBars,
42532
+ {
42533
+ bars: chart.issueBars,
42534
+ bucketCount: axis.bucketCount,
42535
+ firstBar: chart.issueBarRange[0],
42536
+ lastBar: chart.issueBarRange[1],
42537
+ color: chart.issueSeriesColor,
42538
+ direction: "up",
42539
+ animate
42540
+ }
42541
+ ) }, `issues-${windowKey}`)
42542
+ ] }),
42543
+ layoutMode === "real" && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
41881
42544
  ScatterCanvas,
41882
42545
  {
41883
42546
  layers: chart.scatterLayers,
41884
42547
  animate,
42548
+ highlightId: chart.highlightEventId,
41885
42549
  onPointEnter: on.onScatterPointEnter,
41886
42550
  onPointLeave: on.onScatterPointLeave,
41887
42551
  onInspectCommit: on.onInspectCommit
@@ -41890,7 +42554,7 @@ var ScrubberLayout = ({
41890
42554
  ]
41891
42555
  }
41892
42556
  ),
41893
- chart.showCommits && layoutMode === "even" && chart.commits.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime33.jsxs)(
42557
+ chart.showCommits && layoutMode === "even" && chart.commits.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime38.jsxs)(
41894
42558
  "div",
41895
42559
  {
41896
42560
  onMouseEnter: on.onCommitTrackMouseEnter,
@@ -41903,15 +42567,21 @@ var ScrubberLayout = ({
41903
42567
  animation: animate ? FADE_IN_ANIMATION : void 0
41904
42568
  },
41905
42569
  children: [
41906
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(TrackBaseline, { color: GUI_THEME.green, anchor: "top" }),
41907
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42570
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(TrackBaseline, { color: GUI_THEME.green, anchor: "top" }),
42571
+ chart.hoveredCommitBucketIndex !== null && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
42572
+ BucketHighlight,
42573
+ {
42574
+ index: chart.hoveredCommitBucketIndex,
42575
+ bucketCount: axis.bucketCount
42576
+ }
42577
+ ),
42578
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
41908
42579
  VolumeBars,
41909
42580
  {
41910
42581
  bars: chart.commitBars,
41911
42582
  bucketCount: axis.bucketCount,
41912
42583
  firstBar: chart.commitBarRange[0],
41913
42584
  lastBar: chart.commitBarRange[1],
41914
- highlightedIndex: chart.hoveredCommitBucketIndex,
41915
42585
  color: GUI_THEME.green,
41916
42586
  direction: "down",
41917
42587
  animate
@@ -41921,8 +42591,8 @@ var ScrubberLayout = ({
41921
42591
  },
41922
42592
  `commits-${windowKey}`
41923
42593
  ),
41924
- /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(ScrubberNeedle, { fraction: chart.thumbFraction }),
41925
- chart.boardHint && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42594
+ /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(ScrubberNeedle, { fraction: chart.thumbFraction }),
42595
+ chart.boardHint && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
41926
42596
  ScrubberHoverHint,
41927
42597
  {
41928
42598
  ...chart.boardHint,
@@ -41931,7 +42601,7 @@ var ScrubberLayout = ({
41931
42601
  trackWidthPx: chart.trackWidthPx
41932
42602
  }
41933
42603
  ),
41934
- chart.commitHint && /* @__PURE__ */ (0, import_jsx_runtime33.jsx)(
42604
+ chart.commitHint && /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
41935
42605
  ScrubberHoverHint,
41936
42606
  {
41937
42607
  ...chart.commitHint,
@@ -41952,10 +42622,11 @@ var ScrubberLayout = ({
41952
42622
  };
41953
42623
 
41954
42624
  // source/gui/client/components/TimeScrubber.tsx
41955
- var import_jsx_runtime34 = __toESM(require_jsx_runtime(), 1);
42625
+ var import_jsx_runtime39 = __toESM(require_jsx_runtime(), 1);
41956
42626
  var SCRUB_THROTTLE_MS = 120;
41957
42627
  var boardEventRow = (count) => `${count} board event${count === 1 ? "" : "s"}`;
41958
42628
  var COLLAPSED_STORAGE_KEY = "epiq.timeScrubber.collapsed";
42629
+ var LAYOUT_MODE_STORAGE_KEY = "epiq.timeScrubber.layoutMode";
41959
42630
  var SCOPE_STORAGE_KEY = "epiq.timeScrubber.scope";
41960
42631
  var SHOW_ISSUES_STORAGE_KEY = "epiq.timeScrubber.showIssues";
41961
42632
  var SHOW_COMMITS_STORAGE_KEY = "epiq.timeScrubber.showCommits";
@@ -41977,6 +42648,10 @@ var readStoredHiddenIds = () => {
41977
42648
  return /* @__PURE__ */ new Set();
41978
42649
  }
41979
42650
  };
42651
+ var readStoredLayoutMode = () => {
42652
+ const stored = localStorage.getItem(LAYOUT_MODE_STORAGE_KEY);
42653
+ return isLayoutMode(stored) ? stored : "even";
42654
+ };
41980
42655
  var readStoredScope = () => {
41981
42656
  const stored = localStorage.getItem(SCOPE_STORAGE_KEY);
41982
42657
  return isScope(stored) ? stored : "all";
@@ -41991,16 +42666,19 @@ var TimeScrubber = ({
41991
42666
  onRequestHistory,
41992
42667
  boardId,
41993
42668
  connected,
42669
+ socketEpoch,
42670
+ highlightEventId,
41994
42671
  onInspectCommit,
41995
42672
  onBoardFilterChange
41996
42673
  }) => {
41997
42674
  const animate = !usePrefersReducedMotion();
41998
- const trackRef = (0, import_react15.useRef)(null);
41999
- const lastDispatchRef = (0, import_react15.useRef)(0);
42000
- const [layoutMode, setLayoutMode] = (0, import_react15.useState)("even");
42001
- const [scope, setScope] = (0, import_react15.useState)(readStoredScope);
42002
- const [offset, setOffset] = (0, import_react15.useState)(0);
42003
- const [dragFraction, setDragFraction] = (0, import_react15.useState)(null);
42675
+ const trackRef = (0, import_react17.useRef)(null);
42676
+ const lastDispatchRef = (0, import_react17.useRef)(0);
42677
+ const lastTargetRef = (0, import_react17.useRef)(null);
42678
+ const [layoutMode, setLayoutMode] = (0, import_react17.useState)(readStoredLayoutMode);
42679
+ const [scope, setScope] = (0, import_react17.useState)(readStoredScope);
42680
+ const [offset, setOffset] = (0, import_react17.useState)(0);
42681
+ const [dragFraction, setDragFraction] = (0, import_react17.useState)(null);
42004
42682
  const [collapsed, setCollapsed] = usePersistedFlag(
42005
42683
  COLLAPSED_STORAGE_KEY,
42006
42684
  false
@@ -42013,8 +42691,8 @@ var TimeScrubber = ({
42013
42691
  SHOW_COMMITS_STORAGE_KEY,
42014
42692
  true
42015
42693
  );
42016
- const [boardView, setBoardView] = (0, import_react15.useState)(readStoredBoardView);
42017
- const [hiddenIdentityIds, setHiddenIdentityIds] = (0, import_react15.useState)(readStoredHiddenIds);
42694
+ const [boardView, setBoardView] = (0, import_react17.useState)(readStoredBoardView);
42695
+ const [hiddenIdentityIds, setHiddenIdentityIds] = (0, import_react17.useState)(readStoredHiddenIds);
42018
42696
  const [categoriesExpanded, setCategoriesExpanded] = usePersistedFlag(
42019
42697
  CATEGORIES_EXPANDED_STORAGE_KEY,
42020
42698
  false
@@ -42030,18 +42708,26 @@ var TimeScrubber = ({
42030
42708
  const exitMs = layoutMode === "real" && animate ? DOT_EXIT_TOTAL_MS : 0;
42031
42709
  const issueScatter = useExitTransition(showIssues, exitMs);
42032
42710
  const commitScatter = useExitTransition(showCommits, exitMs);
42033
- const [hoveredEvent, setHoveredEvent] = (0, import_react15.useState)(null);
42034
- const [pointerFraction, setPointerFraction] = (0, import_react15.useState)(null);
42035
- const [hoveredCommit, setHoveredCommit] = (0, import_react15.useState)(null);
42036
- const [hoveredBucketIndex, setHoveredBucketIndex] = (0, import_react15.useState)(
42711
+ const [hoveredEvent, setHoveredEvent] = (0, import_react17.useState)(null);
42712
+ const [pointerFraction, setPointerFraction] = (0, import_react17.useState)(null);
42713
+ const [hoveredCommit, setHoveredCommit] = (0, import_react17.useState)(null);
42714
+ const [hoveredBucketIndex, setHoveredBucketIndex] = (0, import_react17.useState)(
42037
42715
  null
42038
42716
  );
42039
- const [hoveredCommitBucketIndex, setHoveredCommitBucketIndex] = (0, import_react15.useState)(null);
42717
+ const [hoveredCommitBucketIndex, setHoveredCommitBucketIndex] = (0, import_react17.useState)(null);
42040
42718
  const periodRange = getPeriodRange(scope, offset);
42041
- (0, import_react15.useEffect)(() => {
42719
+ (0, import_react17.useEffect)(() => {
42042
42720
  if (!connected) return;
42043
- onRequestHistory(periodRange?.start, periodRange?.end, allBoards);
42044
- }, [scope, offset, boardId, allBoards, connected]);
42721
+ pendingRequestId.current = onRequestHistory(
42722
+ periodRange?.start,
42723
+ periodRange?.end,
42724
+ allBoards
42725
+ );
42726
+ }, [scope, offset, boardId, allBoards, connected, socketEpoch]);
42727
+ const changeLayoutMode = (next) => {
42728
+ setLayoutMode(next);
42729
+ localStorage.setItem(LAYOUT_MODE_STORAGE_KEY, next);
42730
+ };
42045
42731
  const changeScope = (nextScope) => {
42046
42732
  armEntrance();
42047
42733
  setScope(nextScope);
@@ -42087,68 +42773,53 @@ var TimeScrubber = ({
42087
42773
  armEntrance();
42088
42774
  setAllBoards(next);
42089
42775
  };
42090
- const steadyBoardId = (0, import_react15.useRef)(boardId);
42091
- if (boardId !== null) steadyBoardId.current = boardId;
42092
- const requestKey = (0, import_react15.useMemo)(
42093
- () => JSON.stringify([scope, offset, steadyBoardId.current, allBoards]),
42094
- [scope, offset, steadyBoardId.current, allBoards]
42095
- );
42096
- const filterKey = (0, import_react15.useMemo)(
42776
+ const filterKey = (0, import_react17.useMemo)(
42097
42777
  () => JSON.stringify([boardView, [...hiddenIdentityIds].sort()]),
42098
42778
  [boardView, hiddenIdentityIds]
42099
42779
  );
42100
- const entrance = (0, import_react15.useRef)(0);
42101
- const armed = (0, import_react15.useRef)(true);
42102
- const seenHistoryId = (0, import_react15.useRef)(historyId);
42780
+ const entrance = (0, import_react17.useRef)(0);
42781
+ const armed = (0, import_react17.useRef)(true);
42103
42782
  const armEntrance = () => {
42104
42783
  armed.current = true;
42105
42784
  };
42106
- const shownRef = (0, import_react15.useRef)({ timeline, commits });
42107
- const heldKey = (0, import_react15.useRef)(null);
42108
- const heldHistoryId = (0, import_react15.useRef)(null);
42109
- const awaiting = (0, import_react15.useRef)(true);
42110
- if (heldKey.current !== requestKey) {
42111
- heldKey.current = requestKey;
42112
- awaiting.current = true;
42113
- }
42114
- if (awaiting.current && heldHistoryId.current !== historyId) {
42115
- heldHistoryId.current = historyId;
42785
+ const shownRef = (0, import_react17.useRef)({ timeline, commits });
42786
+ const pendingRequestId = (0, import_react17.useRef)(null);
42787
+ if (pendingRequestId.current !== null && historyId === pendingRequestId.current) {
42788
+ pendingRequestId.current = null;
42116
42789
  shownRef.current = { timeline, commits };
42117
- awaiting.current = false;
42118
- }
42119
- if (seenHistoryId.current !== historyId) {
42120
- seenHistoryId.current = historyId;
42121
42790
  if (armed.current) {
42122
42791
  armed.current = false;
42123
42792
  entrance.current += 1;
42124
42793
  }
42125
42794
  }
42126
42795
  const shown = shownRef.current;
42127
- const axis = (0, import_react15.useMemo)(() => buildAxis(shown.timeline, shown.commits), [shown]);
42796
+ const axis = (0, import_react17.useMemo)(() => buildAxis(shown.timeline, shown.commits), [shown]);
42128
42797
  const categoriesFiltered = (timeline?.events.length ?? 0) > 0;
42129
- const identities = (0, import_react15.useMemo)(
42798
+ const identities = (0, import_react17.useMemo)(
42130
42799
  () => listIdentities(shown.timeline, boardView),
42131
42800
  [shown, boardView]
42132
42801
  );
42133
- const soleIdentity = (0, import_react15.useMemo)(
42802
+ const soleIdentity = (0, import_react17.useMemo)(
42134
42803
  () => soleVisibleIdentity(identities, hiddenIdentityIds),
42135
42804
  [identities, hiddenIdentityIds]
42136
42805
  );
42137
- const issueCounts = bucketIssueCounts(
42138
- axis,
42139
- shown.timeline,
42140
- boardView,
42141
- hiddenIdentityIds
42806
+ const issueCounts = (0, import_react17.useMemo)(
42807
+ () => bucketIssueCounts(axis, shown.timeline, boardView, hiddenIdentityIds),
42808
+ [axis, shown, boardView, hiddenIdentityIds]
42809
+ );
42810
+ const commitStats = (0, import_react17.useMemo)(
42811
+ () => bucketCommitStats(axis, shown.commits),
42812
+ [axis, shown.commits]
42142
42813
  );
42143
- const commitStats = bucketCommitStats(axis, shown.commits);
42144
- const liveEventDots = (0, import_react15.useMemo)(
42814
+ const liveEventDots = (0, import_react17.useMemo)(
42145
42815
  () => buildEventDots(shown.timeline, boardView, hiddenIdentityIds),
42146
42816
  [shown, boardView, hiddenIdentityIds]
42147
42817
  );
42148
42818
  const dragging = dragFraction !== null;
42149
- const commitPoints = (0, import_react15.useMemo)(
42819
+ const commitPoints = (0, import_react17.useMemo)(
42150
42820
  () => shown.commits.map((commit) => ({
42151
42821
  key: commit.sha,
42822
+ id: null,
42152
42823
  t: commit.time,
42153
42824
  fraction: axis.fractionForTime(commit.time),
42154
42825
  hourFraction: hourFractionForTime(commit.time),
@@ -42161,9 +42832,10 @@ var TimeScrubber = ({
42161
42832
  })),
42162
42833
  [shown.commits, axis]
42163
42834
  );
42164
- const issuePoints = (0, import_react15.useMemo)(
42835
+ const issuePoints = (0, import_react17.useMemo)(
42165
42836
  () => liveEventDots.map((dot) => ({
42166
42837
  key: dot.key,
42838
+ id: dot.id,
42167
42839
  t: dot.t,
42168
42840
  fraction: axis.fractionForTime(dot.t),
42169
42841
  hourFraction: hourFractionForTime(dot.t),
@@ -42175,33 +42847,42 @@ var TimeScrubber = ({
42175
42847
  })),
42176
42848
  [liveEventDots, axis]
42177
42849
  );
42178
- const boardFilter = (0, import_react15.useMemo)(
42850
+ const boardFilter = (0, import_react17.useMemo)(
42179
42851
  () => buildBoardFilter(boardView, identities, hiddenIdentityIds),
42180
42852
  [boardView, identities, hiddenIdentityIds]
42181
42853
  );
42182
- (0, import_react15.useEffect)(() => {
42854
+ (0, import_react17.useEffect)(() => {
42183
42855
  onBoardFilterChange(boardFilter);
42184
42856
  }, [boardFilter]);
42185
- const maxIssueBucketCount = maxOf(issueCounts, 1);
42186
- const maxCommitCount = maxOf(
42187
- Array.from(commitStats.values(), (stats) => stats.count),
42188
- 1
42189
- );
42190
- const issueBars = issueCounts.flatMap(
42191
- (count, index2) => count > 0 ? [{ index: index2, intensity: count / maxIssueBucketCount }] : []
42857
+ const issueBars = (0, import_react17.useMemo)(() => {
42858
+ const max = maxOf(issueCounts, 1);
42859
+ return issueCounts.flatMap(
42860
+ (count, index2) => count > 0 ? [{ index: index2, intensity: count / max }] : []
42861
+ );
42862
+ }, [issueCounts]);
42863
+ const commitBars = (0, import_react17.useMemo)(() => {
42864
+ const max = maxOf(
42865
+ Array.from(commitStats.values(), (stats) => stats.count),
42866
+ 1
42867
+ );
42868
+ return Array.from(commitStats, ([index2, stats]) => ({
42869
+ index: index2,
42870
+ intensity: stats.count / max
42871
+ }));
42872
+ }, [commitStats]);
42873
+ const issueBarRange = (0, import_react17.useMemo)(() => populatedRange(issueBars), [issueBars]);
42874
+ const commitBarRange = (0, import_react17.useMemo)(
42875
+ () => populatedRange(commitBars),
42876
+ [commitBars]
42192
42877
  );
42193
- const commitBars = Array.from(commitStats, ([index2, stats]) => ({
42194
- index: index2,
42195
- intensity: stats.count / maxCommitCount
42196
- }));
42197
- const armedBoardId = (0, import_react15.useRef)(boardId);
42198
- (0, import_react15.useEffect)(() => {
42878
+ const armedBoardId = (0, import_react17.useRef)(boardId);
42879
+ (0, import_react17.useEffect)(() => {
42199
42880
  if (boardId === null || boardId === armedBoardId.current) return;
42200
42881
  armedBoardId.current = boardId;
42201
42882
  armEntrance();
42202
42883
  }, [boardId]);
42203
42884
  const windowKey = `${layoutMode}-${entrance.current}-${filterKey}`;
42204
- const scatterLayers = (0, import_react15.useMemo)(
42885
+ const scatterLayers = (0, import_react17.useMemo)(
42205
42886
  () => [
42206
42887
  ...commitScatter.mounted ? [
42207
42888
  {
@@ -42230,11 +42911,18 @@ var TimeScrubber = ({
42230
42911
  return clamp((clientX - rect.left) / Math.max(1, rect.width), 0, 1);
42231
42912
  };
42232
42913
  const dispatchScrub = (fraction, force) => {
42914
+ if (!connected) return;
42915
+ const target = axis.fractionToTime(fraction);
42916
+ if (target === lastTargetRef.current) return;
42233
42917
  const now = Date.now();
42234
42918
  if (!force && now - lastDispatchRef.current < SCRUB_THROTTLE_MS) return;
42235
42919
  lastDispatchRef.current = now;
42236
- onScrub(axis.fractionToTime(fraction));
42920
+ lastTargetRef.current = target;
42921
+ onScrub(target);
42237
42922
  };
42923
+ (0, import_react17.useEffect)(() => {
42924
+ if (timeTravel.mode !== "scrub") lastTargetRef.current = null;
42925
+ }, [timeTravel.mode]);
42238
42926
  const endDrag = () => {
42239
42927
  if (dragFraction === null) return;
42240
42928
  dispatchScrub(dragFraction, true);
@@ -42251,7 +42939,7 @@ var TimeScrubber = ({
42251
42939
  );
42252
42940
  const bucketTimeAt = (index2) => index2 !== null && index2 < axis.bucketCount ? axis.bucketTimeAt(index2) : void 0;
42253
42941
  const centreFraction = (index2) => (index2 + 0.5) / axis.bucketCount;
42254
- const onScatterPointEnter = (0, import_react15.useCallback)(
42942
+ const onScatterPointEnter = (0, import_react17.useCallback)(
42255
42943
  (point4) => {
42256
42944
  if (point4.commitSha) {
42257
42945
  const commit = shown.commits.find((c) => c.sha === point4.commitSha);
@@ -42271,7 +42959,7 @@ var TimeScrubber = ({
42271
42959
  },
42272
42960
  [shown.commits]
42273
42961
  );
42274
- const onScatterPointLeave = (0, import_react15.useCallback)(() => {
42962
+ const onScatterPointLeave = (0, import_react17.useCallback)(() => {
42275
42963
  setHoveredEvent(null);
42276
42964
  setHoveredCommit(null);
42277
42965
  }, []);
@@ -42316,12 +43004,13 @@ var TimeScrubber = ({
42316
43004
  hoveredEvent?.t ?? hoveredCommit?.commit.time ?? (pointerFraction !== null ? axis.fractionToTime(pointerFraction) : void 0)
42317
43005
  )) ?? null;
42318
43006
  const segmentUnit = chooseSegmentUnit(axis.span);
42319
- return /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(
43007
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
42320
43008
  ScrubberLayout,
42321
43009
  {
42322
43010
  collapsed,
42323
43011
  onToggleCollapsed: () => setCollapsed(!collapsed),
42324
43012
  controls: {
43013
+ connected,
42325
43014
  scope,
42326
43015
  offset,
42327
43016
  periodRange,
@@ -42331,7 +43020,7 @@ var TimeScrubber = ({
42331
43020
  allBoards,
42332
43021
  onChangeScope: changeScope,
42333
43022
  onChangeOffset: changeOffset,
42334
- onChangeLayoutMode: setLayoutMode,
43023
+ onChangeLayoutMode: changeLayoutMode,
42335
43024
  onChangeShowIssues: setShowIssues,
42336
43025
  onChangeShowCommits: setShowCommits,
42337
43026
  onChangeAllBoards: changeAllBoards,
@@ -42360,9 +43049,9 @@ var TimeScrubber = ({
42360
43049
  issueScatter,
42361
43050
  commitScatter,
42362
43051
  issueBars,
42363
- issueBarRange: populatedRange(issueBars),
43052
+ issueBarRange,
42364
43053
  commitBars,
42365
- commitBarRange: populatedRange(commitBars),
43054
+ commitBarRange,
42366
43055
  scatterLayers,
42367
43056
  issueSeriesColor: soleIdentity?.color ?? boardViewColor(boardView),
42368
43057
  dragging,
@@ -42373,7 +43062,9 @@ var TimeScrubber = ({
42373
43062
  hoveredBucketIndex,
42374
43063
  hoveredCommitBucketIndex,
42375
43064
  hoveredSegment: hoveredSegmentTime !== null ? segmentAt(hoveredSegmentTime, segmentUnit) : null,
43065
+ connected,
42376
43066
  thumbFraction: dragFraction ?? confirmedFraction,
43067
+ highlightEventId,
42377
43068
  trackWidthPx: trackRef.current?.clientWidth ?? 0,
42378
43069
  boardHint,
42379
43070
  commitHint,
@@ -42418,7 +43109,7 @@ var TimeScrubber = ({
42418
43109
  };
42419
43110
 
42420
43111
  // source/gui/client/lib/gui-move-issue.tsx
42421
- var toMovePosition = (targetIssues, targetIndex) => {
43112
+ var toMovePosition2 = (targetIssues, targetIndex) => {
42422
43113
  if (targetIssues.length === 0) return { at: "start" };
42423
43114
  if (targetIndex === "end") return { at: "end" };
42424
43115
  const index2 = Math.max(0, Math.min(targetIndex, targetIssues.length));
@@ -42453,7 +43144,7 @@ var findMove = (state, issueId, parentId, targetIndex) => {
42453
43144
  }
42454
43145
  if (!movedIssue || !sourceSwimlaneId || !targetSwimlane) return null;
42455
43146
  const targetIssues = sourceSwimlaneId === targetSwimlane.id ? targetSwimlane.issues.filter((issue) => issue.id !== issueId) : targetSwimlane.issues;
42456
- const position3 = toMovePosition(targetIssues, targetIndex);
43147
+ const position3 = toMovePosition2(targetIssues, targetIndex);
42457
43148
  return {
42458
43149
  movedIssue,
42459
43150
  position: position3,
@@ -42493,6 +43184,12 @@ var moveIssue = (state, setState, send) => (issueIds, parentId, targetIndex) =>
42493
43184
  setState((current) => current ? next : current);
42494
43185
  };
42495
43186
 
43187
+ // source/gui/client/lib/reconnect.ts
43188
+ var BASE_DELAY_MS = 500;
43189
+ var MAX_DELAY_MS = 8e3;
43190
+ var MAX_RECONNECT_ATTEMPTS = 5;
43191
+ var reconnectDelayMs = (attempt) => attempt >= MAX_RECONNECT_ATTEMPTS ? null : Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
43192
+
42496
43193
  // source/lib/utils/node-ref.ts
42497
43194
  var NODE_REF_LENGTH = 7;
42498
43195
  var nodeRef = (id) => id.slice(-NODE_REF_LENGTH).toUpperCase();
@@ -42522,6 +43219,17 @@ var findIssue = (state, issueRefOrId) => findByRefOrId(
42522
43219
  issueRefOrId
42523
43220
  );
42524
43221
  var findBoard = (state, boardRefOrId) => findByRefOrId(state.boards, boardRefOrId);
43222
+ var updateSwimlaneInGuiState = (state, swimlaneId, updateSwimlane) => ({
43223
+ ...state,
43224
+ boards: state.boards.map((board) => ({
43225
+ ...board,
43226
+ swimlanes: board.swimlanes.flatMap((swimlane) => {
43227
+ if (swimlane.id !== swimlaneId) return [swimlane];
43228
+ const next = updateSwimlane(swimlane);
43229
+ return next ? [next] : [];
43230
+ })
43231
+ }))
43232
+ });
42525
43233
  var updateIssueInGuiState = (state, issueId, updateIssue) => ({
42526
43234
  ...state,
42527
43235
  boards: state.boards.map((board) => ({
@@ -42591,6 +43299,10 @@ var createHistoryBuffer = (publish) => {
42591
43299
  var MUTATING_MESSAGE_TYPES = /* @__PURE__ */ new Set([
42592
43300
  "sync",
42593
43301
  "issues:create",
43302
+ "swimlane:create",
43303
+ "swimlane:edit:title",
43304
+ "swimlane:delete",
43305
+ "swimlane:move",
42594
43306
  "issues:move",
42595
43307
  "issue:close",
42596
43308
  "issue:reopen",
@@ -42693,11 +43405,13 @@ var blobToBase64 = (blob) => new Promise((resolve, reject) => {
42693
43405
  });
42694
43406
 
42695
43407
  // source/gui/client/App.tsx
42696
- var import_jsx_runtime35 = __toESM(require_jsx_runtime(), 1);
43408
+ var import_jsx_runtime40 = __toESM(require_jsx_runtime(), 1);
42697
43409
  var EMPTY_COMMENTS = {};
42698
- var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
43410
+ var BOARD_GUTTER = 30;
43411
+ var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
42699
43412
  "div",
42700
43413
  {
43414
+ "data-testid": "ticket-drop-indicator",
42701
43415
  style: {
42702
43416
  height: 2,
42703
43417
  background: GUI_THEME.accent,
@@ -42713,39 +43427,60 @@ var App = () => {
42713
43427
  const legacyIssueMatch = useMatch("/board/:boardId/:issueId");
42714
43428
  const issueId = issueMatch?.params.issueId ?? legacyIssueMatch?.params.issueId;
42715
43429
  const [searchParams, setSearchParams] = useSearchParams();
42716
- const [connected, setConnected] = (0, import_react16.useState)(false);
42717
- const [syncStatus, setSyncStatus] = (0, import_react16.useState)({
43430
+ const [connected, setConnected] = (0, import_react18.useState)(false);
43431
+ const [socketEpoch, setSocketEpoch] = (0, import_react18.useState)(0);
43432
+ const [syncStatus, setSyncStatus] = (0, import_react18.useState)({
42718
43433
  status: "synced",
42719
43434
  msg: "idle"
42720
43435
  });
42721
- const [state, setState] = (0, import_react16.useState)(null);
42722
- const [contributors, setContributors] = (0, import_react16.useState)([]);
42723
- const [history, setHistory] = (0, import_react16.useState)({ requestId: 0, timeline: null, commits: [] });
42724
- const [historyBuffer] = (0, import_react16.useState)(() => createHistoryBuffer(setHistory));
42725
- const [commitInspectError, setCommitInspectError] = (0, import_react16.useState)(
43436
+ const [state, setState] = (0, import_react18.useState)(null);
43437
+ const [noProject, setNoProject] = (0, import_react18.useState)(null);
43438
+ const [contributors, setContributors] = (0, import_react18.useState)([]);
43439
+ const [history, setHistory] = (0, import_react18.useState)({ requestId: 0, timeline: null, commits: [] });
43440
+ const [historyBuffer] = (0, import_react18.useState)(() => createHistoryBuffer(setHistory));
43441
+ const [commitInspectError, setCommitInspectError] = (0, import_react18.useState)(
43442
+ null
43443
+ );
43444
+ const [removeError, setRemoveError] = (0, import_react18.useState)(null);
43445
+ const [actionError, setActionError] = (0, import_react18.useState)(null);
43446
+ const [dragOverSwimlaneId, setDragOverSwimlaneId] = (0, import_react18.useState)(
43447
+ null
43448
+ );
43449
+ const [dropTarget, setDropTarget] = (0, import_react18.useState)(null);
43450
+ const [pickedIssueIds, setPickedIssueIds] = (0, import_react18.useState)([]);
43451
+ const [boardMenuOpen, setBoardMenuOpen] = (0, import_react18.useState)(false);
43452
+ const [createIssueModal, setCreateIssueModal] = (0, import_react18.useState)(null);
43453
+ const [createSwimlaneTitle, setCreateSwimlaneTitle] = (0, import_react18.useState)(
42726
43454
  null
42727
43455
  );
42728
- const [removeError, setRemoveError] = (0, import_react16.useState)(null);
42729
- const [actionError, setActionError] = (0, import_react16.useState)(null);
42730
- const [dragOverSwimlaneId, setDragOverSwimlaneId] = (0, import_react16.useState)(
43456
+ const [renameSwimlane, setRenameSwimlane] = (0, import_react18.useState)(null);
43457
+ const [deleteSwimlaneId, setDeleteSwimlaneId] = (0, import_react18.useState)(null);
43458
+ const [hoveredLogEventId, setHoveredLogEventId] = (0, import_react18.useState)(
42731
43459
  null
42732
43460
  );
42733
- const [dropTarget, setDropTarget] = (0, import_react16.useState)(null);
42734
- const [pickedIssueIds, setPickedIssueIds] = (0, import_react16.useState)([]);
42735
- const [boardMenuOpen, setBoardMenuOpen] = (0, import_react16.useState)(false);
42736
- const [createIssueModal, setCreateIssueModal] = (0, import_react16.useState)(null);
42737
- const boardMenuRef = (0, import_react16.useRef)(null);
42738
- const socketRef = (0, import_react16.useRef)(null);
42739
- const [mutationGate] = (0, import_react16.useState)(createMutationGate);
42740
- const selectedTab = searchParams.get("tab") === "comments" ? "comments" : "overview";
43461
+ const [swimlaneDropEdge, setSwimlaneDropEdge] = (0, import_react18.useState)(null);
43462
+ const boardMenuRef = (0, import_react18.useRef)(null);
43463
+ const socketRef = (0, import_react18.useRef)(null);
43464
+ const [reconnectTick, setReconnectTick] = (0, import_react18.useState)(0);
43465
+ const [reconnectExhausted, setReconnectExhausted] = (0, import_react18.useState)(false);
43466
+ const reconnectAttempts = (0, import_react18.useRef)(0);
43467
+ const reconnectTimer = (0, import_react18.useRef)(null);
43468
+ const reconnectNow = () => {
43469
+ reconnectAttempts.current = 0;
43470
+ setReconnectExhausted(false);
43471
+ setReconnectTick((tick) => tick + 1);
43472
+ };
43473
+ const [mutationGate] = (0, import_react18.useState)(createMutationGate);
43474
+ const tabParam = searchParams.get("tab");
43475
+ const selectedTab = tabParam === "comments" || tabParam === "history" ? tabParam : "overview";
42741
43476
  const navigate = useNavigate();
42742
43477
  const selectedBoard = (state && boardId ? findBoard(state, boardId) : null) ?? state?.boards?.[0] ?? null;
42743
43478
  const selectedBoardId = selectedBoard?.id ?? null;
42744
- const selectedBoardIdRef = (0, import_react16.useRef)(selectedBoardId);
43479
+ const selectedBoardIdRef = (0, import_react18.useRef)(selectedBoardId);
42745
43480
  selectedBoardIdRef.current = selectedBoardId;
42746
43481
  const boardSlug = selectedBoard?.ref ?? boardId;
42747
43482
  const selectedIssue = state && issueId ? findIssue(state, issueId) : null;
42748
- (0, import_react16.useEffect)(() => {
43483
+ (0, import_react18.useEffect)(() => {
42749
43484
  const onPaste = (event) => {
42750
43485
  if (!selectedIssue || selectedIssue.readonly) return;
42751
43486
  const target = event.target;
@@ -42765,9 +43500,9 @@ var App = () => {
42765
43500
  void navigate(`/board/${boardSlug}`);
42766
43501
  };
42767
43502
  const commentsByIssueId = state?.commentsByIssueId ?? EMPTY_COMMENTS;
42768
- const [issueDetail, setIssueDetail] = (0, import_react16.useState)(null);
42769
- const [boardFilter, setBoardFilter] = (0, import_react16.useState)(null);
42770
- const { visibleSwimlanes, hiddenIssueCount } = (0, import_react16.useMemo)(() => {
43503
+ const [issueDetail, setIssueDetail] = (0, import_react18.useState)(null);
43504
+ const [boardFilter, setBoardFilter] = (0, import_react18.useState)(null);
43505
+ const { visibleSwimlanes, hiddenIssueCount } = (0, import_react18.useMemo)(() => {
42771
43506
  const swimlanes = selectedBoard?.swimlanes ?? [];
42772
43507
  if (!boardFilter) return { visibleSwimlanes: swimlanes, hiddenIssueCount: 0 };
42773
43508
  let hidden = 0;
@@ -42785,8 +43520,8 @@ var App = () => {
42785
43520
  return { visibleSwimlanes: visible, hiddenIssueCount: hidden };
42786
43521
  }, [selectedBoard, boardFilter, commentsByIssueId]);
42787
43522
  const attachmentsByIssueId = state?.attachmentsByIssueId ?? {};
42788
- const [attachmentUploadStatus, setAttachmentUploadStatus] = (0, import_react16.useState)({ state: "idle" });
42789
- (0, import_react16.useEffect)(() => {
43523
+ const [attachmentUploadStatus, setAttachmentUploadStatus] = (0, import_react18.useState)({ state: "idle" });
43524
+ (0, import_react18.useEffect)(() => {
42790
43525
  if (!selectedIssue) {
42791
43526
  setIssueDetail(null);
42792
43527
  return;
@@ -42799,29 +43534,50 @@ var App = () => {
42799
43534
  const requestState = () => {
42800
43535
  sendSocketJson(socketRef.current, { type: "state:get" });
42801
43536
  };
42802
- (0, import_react16.useEffect)(() => {
43537
+ (0, import_react18.useEffect)(() => {
42803
43538
  const socket = new WebSocket(
42804
43539
  `ws://${window.location.host}/ws${boardId ? `?boardId=${boardId}` : ""}`
42805
43540
  );
42806
43541
  socketRef.current = socket;
43542
+ let replaced = false;
42807
43543
  socket.addEventListener("open", () => {
42808
43544
  setConnected(true);
43545
+ setSocketEpoch((epoch) => epoch + 1);
43546
+ reconnectAttempts.current = 0;
43547
+ setReconnectExhausted(false);
42809
43548
  mutationGate.reset();
42810
43549
  sendSocketJson(socket, { type: "state:get" });
42811
43550
  });
42812
43551
  socket.addEventListener("close", () => {
42813
- setConnected(false);
42814
- mutationGate.reset();
42815
43552
  if (socketRef.current === socket) {
42816
43553
  socketRef.current = null;
42817
43554
  }
43555
+ if (replaced) return;
43556
+ setConnected(false);
43557
+ mutationGate.reset();
43558
+ const delay = reconnectDelayMs(reconnectAttempts.current);
43559
+ if (delay === null) {
43560
+ setReconnectExhausted(true);
43561
+ return;
43562
+ }
43563
+ reconnectAttempts.current += 1;
43564
+ reconnectTimer.current = window.setTimeout(
43565
+ () => setReconnectTick((tick) => tick + 1),
43566
+ delay
43567
+ );
42818
43568
  });
42819
43569
  socket.addEventListener("message", (event) => {
42820
43570
  const message = JSON.parse(event.data);
42821
43571
  mutationGate.received(message.type);
42822
43572
  if (message.type === "state" && !mutationGate.holdsState()) {
42823
43573
  const nextState = getResultValue(message.payload);
42824
- if (nextState) setState(nextState);
43574
+ if (nextState) {
43575
+ setState(nextState);
43576
+ setNoProject(null);
43577
+ }
43578
+ }
43579
+ if (message.type === "state:unavailable") {
43580
+ setNoProject(message.payload);
42825
43581
  }
42826
43582
  if (message.type === "issue") {
42827
43583
  const detail = getResultValue(message.payload);
@@ -42880,19 +43636,24 @@ var App = () => {
42880
43636
  }
42881
43637
  });
42882
43638
  return () => {
43639
+ replaced = true;
43640
+ if (reconnectTimer.current !== null) {
43641
+ clearTimeout(reconnectTimer.current);
43642
+ reconnectTimer.current = null;
43643
+ }
42883
43644
  if (socketRef.current === socket) {
42884
43645
  socketRef.current = null;
42885
43646
  }
42886
43647
  socket.close();
42887
43648
  };
42888
- }, [boardId, navigate]);
42889
- (0, import_react16.useEffect)(() => {
43649
+ }, [boardId, navigate, reconnectTick]);
43650
+ (0, import_react18.useEffect)(() => {
42890
43651
  const first = state?.boards?.[0];
42891
43652
  if (!boardId && first) {
42892
43653
  void navigate(`/board/${first.ref}`, { replace: true });
42893
43654
  }
42894
43655
  }, [boardId, state, navigate]);
42895
- (0, import_react16.useEffect)(() => {
43656
+ (0, import_react18.useEffect)(() => {
42896
43657
  const close = (event) => {
42897
43658
  if (boardMenuRef.current && !boardMenuRef.current.contains(event.target)) {
42898
43659
  setBoardMenuOpen(false);
@@ -42915,8 +43676,8 @@ var App = () => {
42915
43676
  );
42916
43677
  };
42917
43678
  const clearPicked = () => setPickedIssueIds([]);
42918
- const [bulkTagName, setBulkTagName] = (0, import_react16.useState)("");
42919
- const [bulkAssigneeName, setBulkAssigneeName] = (0, import_react16.useState)("");
43679
+ const [bulkTagName, setBulkTagName] = (0, import_react18.useState)("");
43680
+ const [bulkAssigneeName, setBulkAssigneeName] = (0, import_react18.useState)("");
42920
43681
  const pickedIssues = (state?.boards ?? []).flatMap((board) => board.swimlanes).flatMap((swimlane) => swimlane.issues).filter((issue) => pickedIssueIds.includes(issue.id));
42921
43682
  const forPicked = (act) => {
42922
43683
  for (const issue of pickedIssues) act(issue.id);
@@ -42926,7 +43687,7 @@ var App = () => {
42926
43687
  setPickedIssueIds([nextIssueId]);
42927
43688
  if (!boardSlug) return;
42928
43689
  void navigate(
42929
- `/board/${boardSlug}/issue/${nodeRef(nextIssueId)}?tab=overview`
43690
+ `/board/${boardSlug}/issue/${nodeRef(nextIssueId)}?tab=${selectedTab}`
42930
43691
  );
42931
43692
  };
42932
43693
  const selectIssueComments = (nextIssueId) => {
@@ -43070,7 +43831,7 @@ var App = () => {
43070
43831
  const returnToLive = () => {
43071
43832
  send("time-travel:live", {});
43072
43833
  };
43073
- const requestBoardHistory = (0, import_react16.useCallback)(
43834
+ const requestBoardHistory = (0, import_react18.useCallback)(
43074
43835
  (start2, end, allBoards) => {
43075
43836
  const window2 = start2 !== void 0 ? { start: start2, end } : void 0;
43076
43837
  const requestId = historyBuffer.open();
@@ -43086,33 +43847,34 @@ var App = () => {
43086
43847
  type: "commits:get",
43087
43848
  payload: { ...window2, requestId }
43088
43849
  });
43850
+ return requestId;
43089
43851
  },
43090
43852
  [selectedBoardId]
43091
43853
  );
43092
- const requestContributors = (0, import_react16.useCallback)(() => {
43854
+ const requestContributors = (0, import_react18.useCallback)(() => {
43093
43855
  sendSocketJson(socketRef.current, {
43094
43856
  type: "contributors:get",
43095
43857
  payload: { boardId: selectedBoardIdRef.current }
43096
43858
  });
43097
43859
  }, []);
43098
- (0, import_react16.useEffect)(() => {
43860
+ (0, import_react18.useEffect)(() => {
43099
43861
  setContributors([]);
43100
43862
  setPickedIssueIds([]);
43101
43863
  }, [selectedBoardId]);
43102
- const inspectCommit = (0, import_react16.useCallback)((sha) => {
43864
+ const inspectCommit = (0, import_react18.useCallback)((sha) => {
43103
43865
  sendSocketJson(socketRef.current, { type: "commit:inspect", payload: { sha } });
43104
43866
  }, []);
43105
- (0, import_react16.useEffect)(() => {
43867
+ (0, import_react18.useEffect)(() => {
43106
43868
  if (!commitInspectError) return;
43107
43869
  const timeout = setTimeout(() => setCommitInspectError(null), 8e3);
43108
43870
  return () => clearTimeout(timeout);
43109
43871
  }, [commitInspectError]);
43110
- (0, import_react16.useEffect)(() => {
43872
+ (0, import_react18.useEffect)(() => {
43111
43873
  if (!removeError) return;
43112
43874
  const timeout = setTimeout(() => setRemoveError(null), 8e3);
43113
43875
  return () => clearTimeout(timeout);
43114
43876
  }, [removeError]);
43115
- (0, import_react16.useEffect)(() => {
43877
+ (0, import_react18.useEffect)(() => {
43116
43878
  if (!actionError) return;
43117
43879
  const timeout = setTimeout(() => setActionError(null), 8e3);
43118
43880
  return () => clearTimeout(timeout);
@@ -43138,6 +43900,81 @@ var App = () => {
43138
43900
  parentId
43139
43901
  });
43140
43902
  };
43903
+ const createSwimlane = () => {
43904
+ if (createSwimlaneTitle === null || !selectedBoard) return;
43905
+ const title = createSwimlaneTitle.trim() || "New swimlane";
43906
+ const boardId2 = selectedBoard.id;
43907
+ setCreateSwimlaneTitle(null);
43908
+ setState(
43909
+ (prev) => prev ? {
43910
+ ...prev,
43911
+ boards: prev.boards.map(
43912
+ (board) => board.id === boardId2 ? {
43913
+ ...board,
43914
+ swimlanes: [
43915
+ ...board.swimlanes,
43916
+ {
43917
+ id: `pending-swimlane-${title}`,
43918
+ title,
43919
+ readonly: true,
43920
+ issues: []
43921
+ }
43922
+ ]
43923
+ } : board
43924
+ )
43925
+ } : prev
43926
+ );
43927
+ send("swimlane:create", { title, boardId: boardId2 });
43928
+ };
43929
+ const openRenameSwimlane = (swimlaneId) => {
43930
+ const swimlane = visibleSwimlanes.find((x) => x.id === swimlaneId);
43931
+ if (!swimlane) return;
43932
+ setRenameSwimlane({ swimlaneId, title: swimlane.title });
43933
+ };
43934
+ const submitRenameSwimlane = () => {
43935
+ if (!renameSwimlane) return;
43936
+ const { swimlaneId } = renameSwimlane;
43937
+ const title = renameSwimlane.title.trim();
43938
+ setRenameSwimlane(null);
43939
+ if (!title) return;
43940
+ setState(
43941
+ (prev) => prev ? updateSwimlaneInGuiState(prev, swimlaneId, (swimlane) => ({
43942
+ ...swimlane,
43943
+ title
43944
+ })) : prev
43945
+ );
43946
+ send("swimlane:edit:title", { swimlaneId, title });
43947
+ };
43948
+ const deletingSwimlane = selectedBoard?.swimlanes.find((x) => x.id === deleteSwimlaneId) ?? null;
43949
+ const offline = !connected && state !== null;
43950
+ const shownSwimlanes = (0, import_react18.useMemo)(
43951
+ () => offline ? visibleSwimlanes.map((swimlane) => ({
43952
+ ...swimlane,
43953
+ readonly: true,
43954
+ issues: swimlane.issues.map((issue) => ({ ...issue, readonly: true }))
43955
+ })) : visibleSwimlanes,
43956
+ [offline, visibleSwimlanes]
43957
+ );
43958
+ const dropSwimlane = (swimlaneId) => {
43959
+ const edge = swimlaneDropEdge;
43960
+ setSwimlaneDropEdge(null);
43961
+ if (!edge || !swimlaneId || !selectedBoard) return;
43962
+ const overIndex = visibleSwimlanes.findIndex((x) => x.id === edge.swimlaneId);
43963
+ if (overIndex === -1) return;
43964
+ moveSwimlane(state, setState, send)(
43965
+ swimlaneId,
43966
+ selectedBoard.id,
43967
+ edge.side === "left" ? overIndex : overIndex + 1
43968
+ );
43969
+ };
43970
+ const confirmDeleteSwimlane = () => {
43971
+ if (!deleteSwimlaneId) return;
43972
+ setState(
43973
+ (prev) => prev ? updateSwimlaneInGuiState(prev, deleteSwimlaneId, () => null) : prev
43974
+ );
43975
+ send("swimlane:delete", { swimlaneId: deleteSwimlaneId });
43976
+ setDeleteSwimlaneId(null);
43977
+ };
43141
43978
  const addIssueComment = (issueId2, body) => {
43142
43979
  setState((prev) => {
43143
43980
  if (!prev) return prev;
@@ -43239,7 +44076,17 @@ var App = () => {
43239
44076
  });
43240
44077
  send("issue:comment:delete", { commentId });
43241
44078
  };
43242
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
44079
+ if (noProject) {
44080
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
44081
+ InitProjectScreen,
44082
+ {
44083
+ repoRoot: noProject.repoRoot,
44084
+ message: noProject.message,
44085
+ onRetry: requestState
44086
+ }
44087
+ );
44088
+ }
44089
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
43243
44090
  "div",
43244
44091
  {
43245
44092
  style: {
@@ -43251,38 +44098,39 @@ var App = () => {
43251
44098
  flexDirection: "column"
43252
44099
  },
43253
44100
  children: [
43254
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(GlobalScrollbarStyles, {}),
43255
- commitInspectError && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
44101
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(GlobalScrollbarStyles, {}),
44102
+ commitInspectError && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
43256
44103
  ErrorToast,
43257
44104
  {
43258
44105
  message: `Couldn't open commit diff: ${commitInspectError}`,
43259
44106
  onDismiss: () => setCommitInspectError(null)
43260
44107
  }
43261
44108
  ),
43262
- removeError && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
44109
+ removeError && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
43263
44110
  ErrorToast,
43264
44111
  {
43265
44112
  message: removeError,
43266
44113
  onDismiss: () => setRemoveError(null)
43267
44114
  }
43268
44115
  ),
43269
- actionError && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
44116
+ actionError && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
43270
44117
  ErrorToast,
43271
44118
  {
43272
44119
  message: actionError,
43273
44120
  onDismiss: () => setActionError(null)
43274
44121
  }
43275
44122
  ),
43276
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
44123
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
43277
44124
  Header,
43278
44125
  {
43279
44126
  state,
43280
- connected,
44127
+ connection: connected ? "connected" : !offline ? "connecting" : reconnectExhausted ? "lost" : "reconnecting",
44128
+ onReconnect: reconnectNow,
43281
44129
  scrubbing: state?.timeTravel?.mode === "scrub",
43282
44130
  syncStatus
43283
44131
  }
43284
44132
  ),
43285
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
44133
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
43286
44134
  TimeScrubber,
43287
44135
  {
43288
44136
  timeline: history.timeline,
@@ -43290,31 +44138,33 @@ var App = () => {
43290
44138
  historyId: history.requestId,
43291
44139
  boardId: selectedBoardId,
43292
44140
  connected,
44141
+ socketEpoch,
43293
44142
  onRequestHistory: requestBoardHistory,
43294
44143
  onInspectCommit: inspectCommit,
44144
+ highlightEventId: hoveredLogEventId,
43295
44145
  timeTravel: state?.timeTravel ?? { mode: "live", asOfTime: null },
43296
44146
  onScrub: scrubToTime,
43297
44147
  onReturnToLive: returnToLive,
43298
44148
  onBoardFilterChange: setBoardFilter
43299
44149
  }
43300
44150
  ),
43301
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
44151
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
43302
44152
  "div",
43303
44153
  {
43304
44154
  style: {
43305
44155
  display: "flex",
43306
44156
  flex: 1,
43307
- overflow: "hidden"
44157
+ overflow: "hidden",
44158
+ opacity: offline ? 0.55 : 1,
44159
+ transition: "opacity 160ms ease"
43308
44160
  },
43309
44161
  children: [
43310
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
44162
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
43311
44163
  "main",
43312
44164
  {
43313
44165
  onClick: clearPicked,
43314
44166
  style: {
43315
- // No bottom padding: the board row is the horizontal scroll
43316
- // container, and a gap below it would strand its scrollbar.
43317
- padding: "0 30px 0 30px",
44167
+ padding: "0 0 0 30px",
43318
44168
  flex: 1,
43319
44169
  minHeight: 0,
43320
44170
  display: "flex",
@@ -43322,7 +44172,7 @@ var App = () => {
43322
44172
  overflow: "hidden"
43323
44173
  },
43324
44174
  children: [
43325
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
44175
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
43326
44176
  "div",
43327
44177
  {
43328
44178
  style: {
@@ -43331,7 +44181,7 @@ var App = () => {
43331
44181
  alignItems: "center",
43332
44182
  gap: 10
43333
44183
  },
43334
- children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
44184
+ children: /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
43335
44185
  Dropdown,
43336
44186
  {
43337
44187
  testId: "board-switcher",
@@ -43350,7 +44200,7 @@ var App = () => {
43350
44200
  )
43351
44201
  }
43352
44202
  ),
43353
- /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
44203
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
43354
44204
  "div",
43355
44205
  {
43356
44206
  style: {
@@ -43362,7 +44212,7 @@ var App = () => {
43362
44212
  overflowY: "hidden"
43363
44213
  },
43364
44214
  children: [
43365
- visibleSwimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
44215
+ shownSwimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
43366
44216
  SwimlaneColumn,
43367
44217
  {
43368
44218
  swimlane,
@@ -43374,6 +44224,12 @@ var App = () => {
43374
44224
  onSelectIssue: selectIssue,
43375
44225
  onSelectIssueComments: selectIssueComments,
43376
44226
  onCreateIssue: openCreateIssueModal,
44227
+ onRenameSwimlane: openRenameSwimlane,
44228
+ onDeleteSwimlane: setDeleteSwimlaneId,
44229
+ dropSide: swimlaneDropEdge?.swimlaneId === swimlane.id ? swimlaneDropEdge.side : null,
44230
+ onSwimlaneDragOver: (swimlaneId, side) => setSwimlaneDropEdge({ swimlaneId, side }),
44231
+ onSwimlaneDragEnd: () => setSwimlaneDropEdge(null),
44232
+ onDropSwimlane: dropSwimlane,
43377
44233
  onDropIssue: (issueId2, swimlaneId, targetIndex) => {
43378
44234
  const moving = pickedIssueIds.includes(issueId2) ? pickedIssueIds : [issueId2];
43379
44235
  moveIssue(state, setState, send)(
@@ -43390,14 +44246,16 @@ var App = () => {
43390
44246
  },
43391
44247
  swimlane.id
43392
44248
  )),
43393
- !(selectedIssue && state?.user) && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { style: { width: ASIDE_WIDTH, flexShrink: 0 } })
44249
+ selectedBoard && !selectedBoard.readonly && !offline && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(AddSwimlaneColumn, { onClick: () => setCreateSwimlaneTitle("") }),
44250
+ !(selectedIssue && state?.user) && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { style: { width: ASIDE_WIDTH, flexShrink: 0 } }),
44251
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { style: { width: BOARD_GUTTER, flexShrink: 0 } })
43394
44252
  ]
43395
44253
  }
43396
44254
  )
43397
44255
  ]
43398
44256
  }
43399
44257
  ),
43400
- pickedIssues.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
44258
+ pickedIssues.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
43401
44259
  BulkDetails,
43402
44260
  {
43403
44261
  issues: pickedIssues,
@@ -43415,23 +44273,32 @@ var App = () => {
43415
44273
  onAddAssignee: (assigneeId) => forPicked((id) => addIssueAssignee(id, assigneeId)),
43416
44274
  onRemoveAssignee: (assigneeId) => forPicked((id) => removeIssueAssignee(id, assigneeId)),
43417
44275
  onCloseIssues: () => {
43418
- forPicked(closeIssue);
44276
+ for (const issue of pickedIssues) {
44277
+ if (!issue.isClosed) closeIssue(issue.id);
44278
+ }
43419
44279
  clearPicked();
43420
44280
  },
43421
44281
  onReopenIssues: () => {
43422
- forPicked(reopenIssue);
44282
+ for (const issue of pickedIssues) {
44283
+ if (issue.isClosed) reopenIssue(issue.id);
44284
+ }
43423
44285
  clearPicked();
43424
44286
  },
43425
44287
  onClear: clearPicked
43426
44288
  }
43427
44289
  ),
43428
- pickedIssues.length <= 1 && selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
44290
+ pickedIssues.length <= 1 && selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
43429
44291
  IssueDetails,
43430
44292
  {
43431
44293
  whoAmI: state.user,
43432
- issue: issueDetail?.issueId === selectedIssue.id ? { ...selectedIssue, description: issueDetail.description } : selectedIssue,
44294
+ issue: (() => {
44295
+ const base = issueDetail?.issueId === selectedIssue.id ? { ...selectedIssue, description: issueDetail.description } : selectedIssue;
44296
+ return offline ? { ...base, readonly: true } : base;
44297
+ })(),
43433
44298
  activeTab: selectedTab,
43434
44299
  comments: issueDetail?.issueId === selectedIssue.id ? issueDetail.comments : [],
44300
+ history: issueDetail?.issueId === selectedIssue.id ? issueDetail.history : [],
44301
+ onHoverHistoryEvent: setHoveredLogEventId,
43435
44302
  onChangeTab: changeIssueDetailsTab,
43436
44303
  onClose: closeIssueDetails,
43437
44304
  onEditTitle: editIssueTitle,
@@ -43458,14 +44325,53 @@ var App = () => {
43458
44325
  ]
43459
44326
  }
43460
44327
  ),
43461
- createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
43462
- CreateIssueModal,
44328
+ createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
44329
+ CreateNodeModal,
43463
44330
  {
44331
+ eyebrow: "New issue",
44332
+ fieldLabel: "title",
44333
+ placeholder: "issue name",
43464
44334
  title: createIssueModal.title,
43465
44335
  onChangeTitle: (title) => setCreateIssueModal((prev) => prev ? { ...prev, title } : prev),
43466
44336
  onCreate: createIssue,
43467
44337
  onClose: () => setCreateIssueModal(null)
43468
44338
  }
44339
+ ),
44340
+ renameSwimlane && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
44341
+ CreateNodeModal,
44342
+ {
44343
+ eyebrow: "Rename swimlane",
44344
+ fieldLabel: "title",
44345
+ placeholder: "swimlane name",
44346
+ confirmLabel: "rename",
44347
+ title: renameSwimlane.title,
44348
+ onChangeTitle: (title) => setRenameSwimlane((prev) => prev ? { ...prev, title } : prev),
44349
+ onCreate: submitRenameSwimlane,
44350
+ onClose: () => setRenameSwimlane(null)
44351
+ }
44352
+ ),
44353
+ deletingSwimlane && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
44354
+ ConfirmModal,
44355
+ {
44356
+ eyebrow: "Delete swimlane",
44357
+ heading: `Delete "${deletingSwimlane.title}"?`,
44358
+ body: deletingSwimlane.issues.length > 0 ? `This also deletes the ${deletingSwimlane.issues.length} ticket${deletingSwimlane.issues.length === 1 ? "" : "s"} in it. Their history stays in the event log, but they leave the board.` : "The swimlane is empty, so nothing else goes with it.",
44359
+ confirmLabel: "delete",
44360
+ onConfirm: confirmDeleteSwimlane,
44361
+ onClose: () => setDeleteSwimlaneId(null)
44362
+ }
44363
+ ),
44364
+ createSwimlaneTitle !== null && /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
44365
+ CreateNodeModal,
44366
+ {
44367
+ eyebrow: "New swimlane",
44368
+ fieldLabel: "title",
44369
+ placeholder: "swimlane name",
44370
+ title: createSwimlaneTitle,
44371
+ onChangeTitle: setCreateSwimlaneTitle,
44372
+ onCreate: createSwimlane,
44373
+ onClose: () => setCreateSwimlaneTitle(null)
44374
+ }
43469
44375
  )
43470
44376
  ]
43471
44377
  }
@@ -43473,12 +44379,12 @@ var App = () => {
43473
44379
  };
43474
44380
 
43475
44381
  // source/gui/client/main.tsx
43476
- var import_jsx_runtime36 = __toESM(require_jsx_runtime(), 1);
44382
+ var import_jsx_runtime41 = __toESM(require_jsx_runtime(), 1);
43477
44383
  (0, import_client.createRoot)(document.getElementById("root")).render(
43478
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(BrowserRouter, { children: /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(Routes, { children: [
43479
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Route, { path: "/", element: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(App, {}) }),
43480
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Route, { path: "/board/:boardId/*", element: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(App, {}) }),
43481
- /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Route, { path: "*", element: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(Navigate, { to: "/", replace: true }) })
44384
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(BrowserRouter, { children: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(Routes, { children: [
44385
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Route, { path: "/", element: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(App, {}) }),
44386
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Route, { path: "/board/:boardId/*", element: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(App, {}) }),
44387
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Route, { path: "*", element: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(Navigate, { to: "/", replace: true }) })
43482
44388
  ] }) })
43483
44389
  );
43484
44390
  /*! Bundled license information: