epiq 1.2.3 → 1.3.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
@@ -1328,7 +1328,7 @@ var require_react_dom_development = __commonJS({
1328
1328
  return dispatcher;
1329
1329
  }
1330
1330
  "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
1331
- var React16 = require_react(), Internals = {
1331
+ var React17 = require_react(), Internals = {
1332
1332
  d: {
1333
1333
  f: noop,
1334
1334
  r: function() {
@@ -1346,7 +1346,7 @@ var require_react_dom_development = __commonJS({
1346
1346
  },
1347
1347
  p: 0,
1348
1348
  findDOMNode: null
1349
- }, REACT_PORTAL_TYPE = Symbol.for("react.portal"), ReactSharedInternals = React16.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
1349
+ }, REACT_PORTAL_TYPE = Symbol.for("react.portal"), ReactSharedInternals = React17.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
1350
1350
  "function" === typeof Map && null != Map.prototype && "function" === typeof Map.prototype.forEach && "function" === typeof Set && null != Set.prototype && "function" === typeof Set.prototype.clear && "function" === typeof Set.prototype.forEach || console.error(
1351
1351
  "React depends on Map and Set built-in types. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"
1352
1352
  );
@@ -2881,7 +2881,7 @@ var require_react_dom_client_development = __commonJS({
2881
2881
  "number" === type && getActiveElement(node.ownerDocument) === node || node.defaultValue === "" + value || (node.defaultValue = "" + value);
2882
2882
  }
2883
2883
  function validateOptionProps(element, props) {
2884
- null == props.value && ("object" === typeof props.children && null !== props.children ? React16.Children.forEach(props.children, function(child) {
2884
+ null == props.value && ("object" === typeof props.children && null !== props.children ? React17.Children.forEach(props.children, function(child) {
2885
2885
  null == child || "string" === typeof child || "number" === typeof child || "bigint" === typeof child || didWarnInvalidChild || (didWarnInvalidChild = true, console.error(
2886
2886
  "Cannot infer the option value of complex children. Pass a `value` prop or use a plain string as children to <option>."
2887
2887
  ));
@@ -18513,14 +18513,14 @@ var require_react_dom_client_development = __commonJS({
18513
18513
  ));
18514
18514
  }
18515
18515
  "undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
18516
- var Scheduler = require_scheduler(), React16 = require_react(), ReactDOM = require_react_dom(), assign = Object.assign, REACT_LEGACY_ELEMENT_TYPE = Symbol.for("react.element"), REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy");
18516
+ var Scheduler = require_scheduler(), React17 = require_react(), ReactDOM = require_react_dom(), assign = Object.assign, REACT_LEGACY_ELEMENT_TYPE = Symbol.for("react.element"), REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy");
18517
18517
  Symbol.for("react.scope");
18518
18518
  var REACT_ACTIVITY_TYPE = Symbol.for("react.activity");
18519
18519
  Symbol.for("react.legacy_hidden");
18520
18520
  Symbol.for("react.tracing_marker");
18521
18521
  var REACT_MEMO_CACHE_SENTINEL = Symbol.for("react.memo_cache_sentinel");
18522
18522
  Symbol.for("react.view_transition");
18523
- var MAYBE_ITERATOR_SYMBOL = Symbol.iterator, REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), isArrayImpl = Array.isArray, ReactSharedInternals = React16.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, ReactDOMSharedInternals = ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, NotPending = Object.freeze({
18523
+ var MAYBE_ITERATOR_SYMBOL = Symbol.iterator, REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), isArrayImpl = Array.isArray, ReactSharedInternals = React17.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, ReactDOMSharedInternals = ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, NotPending = Object.freeze({
18524
18524
  pending: false,
18525
18525
  data: null,
18526
18526
  method: null,
@@ -21308,7 +21308,7 @@ var require_react_dom_client_development = __commonJS({
21308
21308
  }
21309
21309
  };
21310
21310
  (function() {
21311
- var isomorphicReactPackageVersion = React16.version;
21311
+ var isomorphicReactPackageVersion = React17.version;
21312
21312
  if ("19.2.6" !== isomorphicReactPackageVersion)
21313
21313
  throw Error(
21314
21314
  'Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:\n - react: ' + (isomorphicReactPackageVersion + "\n - react-dom: 19.2.6\nLearn more: https://react.dev/warnings/version-mismatch")
@@ -21662,18 +21662,18 @@ var require_react_jsx_runtime_development = __commonJS({
21662
21662
  function isValidElement2(object) {
21663
21663
  return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE;
21664
21664
  }
21665
- var React16 = require_react(), REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = React16.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, hasOwnProperty = Object.prototype.hasOwnProperty, isArrayImpl = Array.isArray, createTask = console.createTask ? console.createTask : function() {
21665
+ var React17 = require_react(), REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = Symbol.for("react.memo"), REACT_LAZY_TYPE = Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = Symbol.for("react.activity"), REACT_CLIENT_REFERENCE = Symbol.for("react.client.reference"), ReactSharedInternals = React17.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, hasOwnProperty = Object.prototype.hasOwnProperty, isArrayImpl = Array.isArray, createTask = console.createTask ? console.createTask : function() {
21666
21666
  return null;
21667
21667
  };
21668
- React16 = {
21668
+ React17 = {
21669
21669
  react_stack_bottom_frame: function(callStackForError) {
21670
21670
  return callStackForError();
21671
21671
  }
21672
21672
  };
21673
21673
  var specialPropKeyWarningShown;
21674
21674
  var didWarnAboutElementRef = {};
21675
- var unknownOwnerDebugStack = React16.react_stack_bottom_frame.bind(
21676
- React16,
21675
+ var unknownOwnerDebugStack = React17.react_stack_bottom_frame.bind(
21676
+ React17,
21677
21677
  UnknownOwner
21678
21678
  )();
21679
21679
  var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));
@@ -24492,7 +24492,7 @@ function useViewTransitionState(to, { relative } = {}) {
24492
24492
  }
24493
24493
 
24494
24494
  // source/gui/client/App.tsx
24495
- var import_react8 = __toESM(require_react(), 1);
24495
+ var import_react9 = __toESM(require_react(), 1);
24496
24496
 
24497
24497
  // source/gui/client/components/Button.tsx
24498
24498
  var import_react = __toESM(require_react(), 1);
@@ -24911,12 +24911,13 @@ var User = ({
24911
24911
  );
24912
24912
 
24913
24913
  // source/version.ts
24914
- var EPIQ_VERSION = "1.2.3";
24914
+ var EPIQ_VERSION = "1.3.0";
24915
24915
 
24916
24916
  // source/gui/client/components/Header.tsx
24917
24917
  var import_jsx_runtime6 = __toESM(require_jsx_runtime(), 1);
24918
24918
  var Header = ({ state, connected, syncStatus }) => {
24919
24919
  const syncColor = syncStatus.status === "synced" ? GUI_THEME.green : syncStatus.status === "failed" ? GUI_THEME.red : GUI_THEME.accent;
24920
+ const syncLabel = syncStatus.status === "failed" ? "-" : syncStatus.msg.toLowerCase();
24920
24921
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
24921
24922
  Panel,
24922
24923
  {
@@ -24986,12 +24987,17 @@ var Header = ({ state, connected, syncStatus }) => {
24986
24987
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
24987
24988
  "span",
24988
24989
  {
24990
+ title: syncStatus.status === "failed" ? syncStatus.msg : void 0,
24989
24991
  style: {
24990
24992
  color: GUI_THEME.dim,
24991
24993
  minWidth: 72,
24994
+ maxWidth: 220,
24995
+ overflow: "hidden",
24996
+ textOverflow: "ellipsis",
24997
+ whiteSpace: "nowrap",
24992
24998
  textAlign: "right"
24993
24999
  },
24994
- children: connected ? syncStatus.msg.toLowerCase() : "-"
25000
+ children: connected ? syncLabel : "-"
24995
25001
  }
24996
25002
  ),
24997
25003
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -25042,7 +25048,7 @@ var Header = ({ state, connected, syncStatus }) => {
25042
25048
  };
25043
25049
 
25044
25050
  // source/gui/client/components/IssueDetails.tsx
25045
- var import_react6 = __toESM(require_react(), 1);
25051
+ var import_react7 = __toESM(require_react(), 1);
25046
25052
 
25047
25053
  // source/gui/client/components/Aside.tsx
25048
25054
  var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
@@ -25108,7 +25114,7 @@ var CopyRef = ({ refValue }) => {
25108
25114
  onMouseLeave: (event) => {
25109
25115
  event.currentTarget.style.color = copied ? GUI_THEME.green : GUI_THEME.dim;
25110
25116
  },
25111
- children: copied ? "copied!" : `#${refValue}`
25117
+ children: copied ? "copied!" : refValue
25112
25118
  }
25113
25119
  );
25114
25120
  };
@@ -25182,9 +25188,299 @@ var AddRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("di
25182
25188
  var ChipRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10 }, children });
25183
25189
  var Empty = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { display: "inline-block", marginTop: 8, color: GUI_THEME.dim }, children });
25184
25190
 
25185
- // source/gui/client/components/IssueComments.tsx
25191
+ // source/gui/client/components/IssueAttachments.tsx
25186
25192
  var import_react5 = __toESM(require_react(), 1);
25187
25193
 
25194
+ // source/gui/client/components/Section.tsx
25195
+ var import_jsx_runtime11 = __toESM(require_jsx_runtime(), 1);
25196
+ var Section = ({
25197
+ title,
25198
+ action,
25199
+ children,
25200
+ first = false
25201
+ }) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
25202
+ "section",
25203
+ {
25204
+ style: {
25205
+ padding: first ? "0px 0 14px 0" : "14px 0",
25206
+ borderTop: first ? "none" : `1px solid ${GUI_THEME.line}`
25207
+ },
25208
+ children: [
25209
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
25210
+ "div",
25211
+ {
25212
+ style: {
25213
+ display: "flex",
25214
+ justifyContent: "space-between",
25215
+ alignItems: "center",
25216
+ gap: 12
25217
+ },
25218
+ children: [
25219
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
25220
+ "span",
25221
+ {
25222
+ style: {
25223
+ color: GUI_THEME.secondary,
25224
+ fontSize: 10,
25225
+ textTransform: "uppercase",
25226
+ letterSpacing: "0.08em"
25227
+ },
25228
+ children: title
25229
+ }
25230
+ ),
25231
+ action
25232
+ ]
25233
+ }
25234
+ ),
25235
+ children
25236
+ ]
25237
+ }
25238
+ );
25239
+
25240
+ // source/gui/client/components/IssueAttachments.tsx
25241
+ var import_jsx_runtime12 = __toESM(require_jsx_runtime(), 1);
25242
+ var IssueAttachments = ({
25243
+ issueId,
25244
+ readonly,
25245
+ attachments,
25246
+ uploadStatus,
25247
+ onUploadFiles,
25248
+ onDeleteAttachment
25249
+ }) => {
25250
+ const [dragging, setDragging] = (0, import_react5.useState)(false);
25251
+ const [lightbox, setLightbox] = (0, import_react5.useState)(null);
25252
+ const [broken, setBroken] = (0, import_react5.useState)({});
25253
+ (0, import_react5.useEffect)(() => {
25254
+ setLightbox(null);
25255
+ setDragging(false);
25256
+ setBroken({});
25257
+ }, [issueId]);
25258
+ (0, import_react5.useEffect)(() => {
25259
+ if (!lightbox) return;
25260
+ const onKeyDown = (event) => {
25261
+ if (event.key === "Escape") setLightbox(null);
25262
+ };
25263
+ window.addEventListener("keydown", onKeyDown);
25264
+ return () => window.removeEventListener("keydown", onKeyDown);
25265
+ }, [lightbox]);
25266
+ const canUpload = !readonly && Boolean(onUploadFiles);
25267
+ const handleDrop = (event) => {
25268
+ event.preventDefault();
25269
+ setDragging(false);
25270
+ if (!canUpload) return;
25271
+ const files = Array.from(event.dataTransfer.files).filter(
25272
+ (file) => file.type.startsWith("image/")
25273
+ );
25274
+ if (files.length > 0) onUploadFiles?.(issueId, files);
25275
+ };
25276
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25277
+ Section,
25278
+ {
25279
+ title: `Attachments${attachments.length ? ` (${attachments.length})` : ""}`,
25280
+ children: [
25281
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25282
+ "div",
25283
+ {
25284
+ onDragOver: (event) => {
25285
+ if (!canUpload) return;
25286
+ event.preventDefault();
25287
+ setDragging(true);
25288
+ },
25289
+ onDragLeave: () => setDragging(false),
25290
+ onDrop: handleDrop,
25291
+ style: {
25292
+ marginTop: 8,
25293
+ padding: dragging ? 10 : 0,
25294
+ border: dragging ? `1px dashed ${GUI_THEME.accent}` : "1px dashed transparent",
25295
+ borderRadius: 4,
25296
+ transition: "padding 0.12s ease, border-color 0.12s ease"
25297
+ },
25298
+ children: [
25299
+ attachments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Empty, { children: canUpload ? "No attachments \u2014 drop an image here" : "No attachments" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25300
+ "div",
25301
+ {
25302
+ style: {
25303
+ display: "flex",
25304
+ flexWrap: "wrap",
25305
+ gap: 8
25306
+ },
25307
+ children: attachments.map((attachment) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25308
+ "div",
25309
+ {
25310
+ style: { position: "relative", width: 72, height: 72 },
25311
+ children: [
25312
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25313
+ "button",
25314
+ {
25315
+ type: "button",
25316
+ disabled: broken[attachment.id],
25317
+ onClick: () => setLightbox(attachment),
25318
+ title: `${attachment.name} (${Math.max(
25319
+ 1,
25320
+ Math.round(attachment.bytes / 1024)
25321
+ )} KB)`,
25322
+ style: {
25323
+ padding: 0,
25324
+ border: `1px solid ${GUI_THEME.line}`,
25325
+ borderRadius: 4,
25326
+ background: GUI_THEME.panel2,
25327
+ cursor: broken[attachment.id] ? "default" : "zoom-in",
25328
+ overflow: "hidden",
25329
+ width: "100%",
25330
+ height: "100%"
25331
+ },
25332
+ children: broken[attachment.id] ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25333
+ "span",
25334
+ {
25335
+ title: "Attachment unavailable (missing, corrupt, or over the size cap)",
25336
+ style: {
25337
+ display: "flex",
25338
+ alignItems: "center",
25339
+ justifyContent: "center",
25340
+ width: "100%",
25341
+ height: "100%",
25342
+ color: GUI_THEME.dim2,
25343
+ fontSize: 9,
25344
+ padding: 4,
25345
+ textAlign: "center"
25346
+ },
25347
+ children: "unavailable"
25348
+ }
25349
+ ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25350
+ "img",
25351
+ {
25352
+ src: `/media/${attachment.fileName}`,
25353
+ alt: attachment.name,
25354
+ loading: "lazy",
25355
+ onError: () => setBroken((prev) => ({ ...prev, [attachment.id]: true })),
25356
+ style: {
25357
+ width: "100%",
25358
+ height: "100%",
25359
+ objectFit: "cover",
25360
+ display: "block"
25361
+ }
25362
+ }
25363
+ )
25364
+ }
25365
+ ),
25366
+ attachment.canDelete && !readonly && onDeleteAttachment && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25367
+ "button",
25368
+ {
25369
+ type: "button",
25370
+ title: "Delete attachment",
25371
+ onClick: () => onDeleteAttachment(issueId, attachment.id),
25372
+ style: {
25373
+ position: "absolute",
25374
+ top: 2,
25375
+ right: 2,
25376
+ width: 16,
25377
+ height: 16,
25378
+ lineHeight: "14px",
25379
+ padding: 0,
25380
+ border: `1px solid ${GUI_THEME.line}`,
25381
+ borderRadius: 3,
25382
+ background: "rgba(4, 5, 8, 0.75)",
25383
+ color: GUI_THEME.secondary,
25384
+ fontSize: 10,
25385
+ cursor: "pointer"
25386
+ },
25387
+ children: "\xD7"
25388
+ }
25389
+ )
25390
+ ]
25391
+ },
25392
+ attachment.id
25393
+ ))
25394
+ }
25395
+ ),
25396
+ canUpload && attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25397
+ "div",
25398
+ {
25399
+ style: {
25400
+ marginTop: 8,
25401
+ color: GUI_THEME.dim2,
25402
+ fontSize: 10
25403
+ },
25404
+ children: "drop an image to attach"
25405
+ }
25406
+ ),
25407
+ uploadStatus.state === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25408
+ "div",
25409
+ {
25410
+ style: {
25411
+ marginTop: 8,
25412
+ color: GUI_THEME.secondary,
25413
+ fontSize: 11
25414
+ },
25415
+ children: [
25416
+ "uploading ",
25417
+ uploadStatus.name,
25418
+ "\u2026"
25419
+ ]
25420
+ }
25421
+ ),
25422
+ uploadStatus.state === "error" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25423
+ "div",
25424
+ {
25425
+ style: {
25426
+ marginTop: 8,
25427
+ color: GUI_THEME.red,
25428
+ fontSize: 11
25429
+ },
25430
+ children: uploadStatus.message
25431
+ }
25432
+ )
25433
+ ]
25434
+ }
25435
+ ),
25436
+ lightbox && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25437
+ "div",
25438
+ {
25439
+ onClick: () => setLightbox(null),
25440
+ style: {
25441
+ position: "fixed",
25442
+ inset: 0,
25443
+ zIndex: 100,
25444
+ display: "flex",
25445
+ alignItems: "center",
25446
+ justifyContent: "center",
25447
+ flexDirection: "column",
25448
+ gap: 10,
25449
+ background: "rgba(4, 5, 8, 0.88)",
25450
+ cursor: "zoom-out"
25451
+ },
25452
+ children: [
25453
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25454
+ "img",
25455
+ {
25456
+ src: `/media/${lightbox.fileName}`,
25457
+ alt: lightbox.name,
25458
+ style: {
25459
+ maxWidth: "90vw",
25460
+ maxHeight: "85vh",
25461
+ borderRadius: 6,
25462
+ border: `1px solid ${GUI_THEME.line}`
25463
+ }
25464
+ }
25465
+ ),
25466
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 11 }, children: [
25467
+ lightbox.name,
25468
+ " \xB7 ",
25469
+ Math.max(1, Math.round(lightbox.bytes / 1024)),
25470
+ " ",
25471
+ "KB \xB7 esc to close"
25472
+ ] })
25473
+ ]
25474
+ }
25475
+ )
25476
+ ]
25477
+ }
25478
+ );
25479
+ };
25480
+
25481
+ // source/gui/client/components/IssueComments.tsx
25482
+ var import_react6 = __toESM(require_react(), 1);
25483
+
25188
25484
  // source/gui/client/lib/gui-format.helper.ts
25189
25485
  var timeAgo = (timestampMs) => {
25190
25486
  const diff = Date.now() - timestampMs;
@@ -25205,7 +25501,7 @@ var timeAgo = (timestampMs) => {
25205
25501
  };
25206
25502
 
25207
25503
  // source/gui/client/components/IssueComments.tsx
25208
- var import_jsx_runtime11 = __toESM(require_jsx_runtime(), 1);
25504
+ var import_jsx_runtime13 = __toESM(require_jsx_runtime(), 1);
25209
25505
  var IssueComments = ({
25210
25506
  whoAmI,
25211
25507
  issueId,
@@ -25214,15 +25510,15 @@ var IssueComments = ({
25214
25510
  onAddComment,
25215
25511
  onDeleteComment
25216
25512
  }) => {
25217
- const [body, setBody] = (0, import_react5.useState)("");
25513
+ const [body, setBody] = (0, import_react6.useState)("");
25218
25514
  const addComment = () => {
25219
25515
  const nextBody = body.trim();
25220
25516
  if (!nextBody) return;
25221
25517
  onAddComment?.(issueId, nextBody);
25222
25518
  setBody("");
25223
25519
  };
25224
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { children: [
25225
- comments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Empty, { children: "No comments" }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
25520
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { children: [
25521
+ comments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Empty, { children: "No comments" }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25226
25522
  "div",
25227
25523
  {
25228
25524
  style: {
@@ -25232,7 +25528,7 @@ var IssueComments = ({
25232
25528
  background: GUI_THEME.tertiary
25233
25529
  },
25234
25530
  children: [
25235
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
25531
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25236
25532
  "div",
25237
25533
  {
25238
25534
  style: {
@@ -25242,15 +25538,15 @@ var IssueComments = ({
25242
25538
  marginBottom: 8
25243
25539
  },
25244
25540
  children: [
25245
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 10 }, children: [
25541
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 10 }, children: [
25246
25542
  comment.author.name ?? "unknown",
25247
- comment.createdAt && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { style: { color: GUI_THEME.dim2 }, children: [
25543
+ comment.createdAt && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { style: { color: GUI_THEME.dim2 }, children: [
25248
25544
  " ",
25249
25545
  "\xB7 ",
25250
25546
  timeAgo(comment.createdAt)
25251
25547
  ] })
25252
25548
  ] }),
25253
- comment.author.id === whoAmI.id && onDeleteComment && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
25549
+ comment.author.id === whoAmI.id && onDeleteComment && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25254
25550
  Button,
25255
25551
  {
25256
25552
  variant: "ghost",
@@ -25265,7 +25561,7 @@ var IssueComments = ({
25265
25561
  ]
25266
25562
  }
25267
25563
  ),
25268
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
25564
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25269
25565
  "div",
25270
25566
  {
25271
25567
  style: {
@@ -25280,8 +25576,8 @@ var IssueComments = ({
25280
25576
  },
25281
25577
  comment.id
25282
25578
  )) }),
25283
- !readonly && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { marginTop: 18 }, children: [
25284
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
25579
+ !readonly && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: { marginTop: 18 }, children: [
25580
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25285
25581
  Textarea,
25286
25582
  {
25287
25583
  maxLength: 120,
@@ -25296,64 +25592,18 @@ var IssueComments = ({
25296
25592
  style: { minHeight: 45 }
25297
25593
  }
25298
25594
  ),
25299
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ActionRow, { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Button, { onClick: addComment, children: "comment" }) })
25595
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ActionRow, { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { onClick: addComment, children: "comment" }) })
25300
25596
  ] })
25301
25597
  ] });
25302
25598
  };
25303
25599
 
25304
- // source/gui/client/components/Section.tsx
25305
- var import_jsx_runtime12 = __toESM(require_jsx_runtime(), 1);
25306
- var Section = ({
25307
- title,
25308
- action,
25309
- children,
25310
- first = false
25311
- }) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25312
- "section",
25313
- {
25314
- style: {
25315
- padding: first ? "0px 0 14px 0" : "14px 0",
25316
- borderTop: first ? "none" : `1px solid ${GUI_THEME.line}`
25317
- },
25318
- children: [
25319
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25320
- "div",
25321
- {
25322
- style: {
25323
- display: "flex",
25324
- justifyContent: "space-between",
25325
- alignItems: "center",
25326
- gap: 12
25327
- },
25328
- children: [
25329
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25330
- "span",
25331
- {
25332
- style: {
25333
- color: GUI_THEME.secondary,
25334
- fontSize: 10,
25335
- textTransform: "uppercase",
25336
- letterSpacing: "0.08em"
25337
- },
25338
- children: title
25339
- }
25340
- ),
25341
- action
25342
- ]
25343
- }
25344
- ),
25345
- children
25346
- ]
25347
- }
25348
- );
25349
-
25350
25600
  // source/gui/client/components/Tabs.tsx
25351
- var import_jsx_runtime13 = __toESM(require_jsx_runtime(), 1);
25601
+ var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
25352
25602
  var Tabs = ({
25353
25603
  activeTab,
25354
25604
  tabs,
25355
25605
  onChange
25356
- }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25606
+ }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25357
25607
  "div",
25358
25608
  {
25359
25609
  style: {
@@ -25364,7 +25614,7 @@ var Tabs = ({
25364
25614
  },
25365
25615
  children: tabs.map((tab) => {
25366
25616
  const active = tab.id === activeTab;
25367
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25617
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25368
25618
  "button",
25369
25619
  {
25370
25620
  type: "button",
@@ -25382,7 +25632,7 @@ var Tabs = ({
25382
25632
  },
25383
25633
  children: [
25384
25634
  tab.label,
25385
- typeof tab.count === "number" && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25635
+ typeof tab.count === "number" && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25386
25636
  "span",
25387
25637
  {
25388
25638
  style: { color: tab.count ? GUI_THEME.secondary : GUI_THEME.dim },
@@ -25403,7 +25653,7 @@ var Tabs = ({
25403
25653
  );
25404
25654
 
25405
25655
  // source/gui/client/components/IssueDetails.tsx
25406
- var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
25656
+ var import_jsx_runtime15 = __toESM(require_jsx_runtime(), 1);
25407
25657
  var IssueDetails = ({
25408
25658
  whoAmI,
25409
25659
  comments,
@@ -25421,18 +25671,22 @@ var IssueDetails = ({
25421
25671
  onReopenIssue,
25422
25672
  onAddComment,
25423
25673
  onDeleteComment,
25674
+ attachments,
25675
+ attachmentUploadStatus,
25676
+ onUploadAttachments,
25677
+ onDeleteAttachment,
25424
25678
  knownTags: tags,
25425
25679
  knownAssignees: assignees
25426
25680
  }) => {
25427
- const [title, setTitle] = (0, import_react6.useState)("");
25428
- const [description, setDescription] = (0, import_react6.useState)("");
25429
- const [tagName, setTagName] = (0, import_react6.useState)("");
25430
- const [assigneeName, setAssigneeName] = (0, import_react6.useState)("");
25431
- const [editingTitle, setEditingTitle] = (0, import_react6.useState)(false);
25432
- const [editingDescription, setEditingDescription] = (0, import_react6.useState)(false);
25433
- const [addingTag, setAddingTag] = (0, import_react6.useState)(false);
25434
- const [addingAssignee, setAddingAssignee] = (0, import_react6.useState)(false);
25435
- (0, import_react6.useEffect)(() => {
25681
+ const [title, setTitle] = (0, import_react7.useState)("");
25682
+ const [description, setDescription] = (0, import_react7.useState)("");
25683
+ const [tagName, setTagName] = (0, import_react7.useState)("");
25684
+ const [assigneeName, setAssigneeName] = (0, import_react7.useState)("");
25685
+ const [editingTitle, setEditingTitle] = (0, import_react7.useState)(false);
25686
+ const [editingDescription, setEditingDescription] = (0, import_react7.useState)(false);
25687
+ const [addingTag, setAddingTag] = (0, import_react7.useState)(false);
25688
+ const [addingAssignee, setAddingAssignee] = (0, import_react7.useState)(false);
25689
+ (0, import_react7.useEffect)(() => {
25436
25690
  setTitle(issue?.title ?? "");
25437
25691
  setDescription(issue?.description ?? "");
25438
25692
  setTagName("");
@@ -25492,9 +25746,9 @@ var IssueDetails = ({
25492
25746
  const availableAssignees = assignees.filter(
25493
25747
  (assignee) => !issue?.assignees.some((issueAssignee) => issueAssignee.id === assignee.id)
25494
25748
  );
25495
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Aside, { children: issue ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
25496
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(FormHeader, { children: [
25497
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25749
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Aside, { children: issue ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
25750
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(FormHeader, { children: [
25751
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25498
25752
  "span",
25499
25753
  {
25500
25754
  style: {
@@ -25503,19 +25757,19 @@ var IssueDetails = ({
25503
25757
  textTransform: "uppercase",
25504
25758
  letterSpacing: "0.08em"
25505
25759
  },
25506
- children: issue.ref && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CopyRef, { refValue: issue.ref })
25760
+ children: issue.ref && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CopyRef, { refValue: issue.ref })
25507
25761
  }
25508
25762
  ),
25509
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { variant: "ghost", onClick: onClose, children: "\xD7" })
25763
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "ghost", onClick: onClose, children: "\xD7" })
25510
25764
  ] }),
25511
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Tabs, { tabs, activeTab, onChange: onChangeTab }),
25512
- activeTab === "overview" && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
25513
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25765
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Tabs, { tabs, activeTab, onChange: onChangeTab }),
25766
+ activeTab === "overview" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
25767
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25514
25768
  Section,
25515
25769
  {
25516
25770
  first: true,
25517
25771
  title: "Title",
25518
- action: !issue.readonly && !editingTitle && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25772
+ action: !issue.readonly && !editingTitle && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25519
25773
  Button,
25520
25774
  {
25521
25775
  variant: "ghost",
@@ -25523,8 +25777,8 @@ var IssueDetails = ({
25523
25777
  children: "edit"
25524
25778
  }
25525
25779
  ),
25526
- children: editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
25527
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25780
+ children: editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
25781
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25528
25782
  Input,
25529
25783
  {
25530
25784
  value: title,
@@ -25536,11 +25790,11 @@ var IssueDetails = ({
25536
25790
  }
25537
25791
  }
25538
25792
  ),
25539
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(ActionRow, { children: [
25540
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { onClick: saveTitle, children: "save" }),
25541
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { variant: "ghost", onClick: cancelTitle, children: "cancel" })
25793
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(ActionRow, { children: [
25794
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: saveTitle, children: "save" }),
25795
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "ghost", onClick: cancelTitle, children: "cancel" })
25542
25796
  ] })
25543
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25797
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25544
25798
  "div",
25545
25799
  {
25546
25800
  style: {
@@ -25555,11 +25809,11 @@ var IssueDetails = ({
25555
25809
  )
25556
25810
  }
25557
25811
  ),
25558
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25812
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25559
25813
  Section,
25560
25814
  {
25561
25815
  title: "Description",
25562
- action: !issue.readonly && !editingDescription && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25816
+ action: !issue.readonly && !editingDescription && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25563
25817
  Button,
25564
25818
  {
25565
25819
  variant: "ghost",
@@ -25567,8 +25821,8 @@ var IssueDetails = ({
25567
25821
  children: "edit"
25568
25822
  }
25569
25823
  ),
25570
- children: editingDescription ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
25571
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25824
+ children: editingDescription ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
25825
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25572
25826
  Textarea,
25573
25827
  {
25574
25828
  value: description,
@@ -25583,31 +25837,34 @@ var IssueDetails = ({
25583
25837
  }
25584
25838
  }
25585
25839
  ),
25586
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(ActionRow, { children: [
25587
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { onClick: saveDescription, children: "save" }),
25588
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { variant: "ghost", onClick: cancelDescription, children: "cancel" })
25840
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(ActionRow, { children: [
25841
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: saveDescription, children: "save" }),
25842
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "ghost", onClick: cancelDescription, children: "cancel" })
25589
25843
  ] })
25590
- ] }) : issue.description ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25844
+ ] }) : issue.description ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25591
25845
  "p",
25592
25846
  {
25593
25847
  style: {
25594
25848
  lineHeight: 1.55,
25595
25849
  whiteSpace: "pre-wrap",
25596
25850
  margin: "8px 0 0",
25851
+ padding: "12px 16px",
25852
+ background: "rgba(0, 0, 0, 0.22)",
25853
+ borderRadius: 8,
25597
25854
  color: GUI_THEME.primary
25598
25855
  },
25599
25856
  children: issue.description
25600
25857
  }
25601
- ) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Empty, { children: "No description" })
25858
+ ) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Empty, { children: "No description" })
25602
25859
  }
25603
25860
  ),
25604
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25861
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25605
25862
  Section,
25606
25863
  {
25607
25864
  title: "Tags",
25608
- action: !issue.readonly && !addingTag && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { variant: "ghost", onClick: () => setAddingTag(true), children: "+" }) || addingTag && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { variant: "ghost", onClick: () => setAddingTag(false), children: "-" }),
25865
+ action: !issue.readonly && !addingTag && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "ghost", onClick: () => setAddingTag(true), children: "+" }) || addingTag && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "ghost", onClick: () => setAddingTag(false), children: "-" }),
25609
25866
  children: [
25610
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChipRow, { children: issue.tags.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Empty, { children: "No tags" }) : issue.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25867
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChipRow, { children: issue.tags.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Empty, { children: "No tags" }) : issue.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25611
25868
  Button,
25612
25869
  {
25613
25870
  variant: "chip",
@@ -25623,7 +25880,7 @@ var IssueDetails = ({
25623
25880
  },
25624
25881
  tag.id
25625
25882
  )) }),
25626
- addingTag && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25883
+ addingTag && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25627
25884
  Button,
25628
25885
  {
25629
25886
  variant: "chip",
@@ -25638,8 +25895,8 @@ var IssueDetails = ({
25638
25895
  },
25639
25896
  tag.id
25640
25897
  )) }),
25641
- addingTag && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(AddRow, { children: [
25642
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25898
+ addingTag && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(AddRow, { children: [
25899
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25643
25900
  Input,
25644
25901
  {
25645
25902
  value: tagName,
@@ -25655,23 +25912,23 @@ var IssueDetails = ({
25655
25912
  }
25656
25913
  }
25657
25914
  ),
25658
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { onClick: addTag, children: "add" })
25915
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: addTag, children: "add" })
25659
25916
  ] })
25660
25917
  ]
25661
25918
  }
25662
25919
  ),
25663
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25920
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25664
25921
  Section,
25665
25922
  {
25666
25923
  title: "Assignees",
25667
- action: !issue.readonly && !addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25924
+ action: !issue.readonly && !addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25668
25925
  Button,
25669
25926
  {
25670
25927
  variant: "ghost",
25671
25928
  onClick: () => setAddingAssignee(true),
25672
25929
  children: "+"
25673
25930
  }
25674
- ) || addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25931
+ ) || addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25675
25932
  Button,
25676
25933
  {
25677
25934
  variant: "ghost",
@@ -25680,7 +25937,7 @@ var IssueDetails = ({
25680
25937
  }
25681
25938
  ),
25682
25939
  children: [
25683
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChipRow, { children: issue.assignees.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Empty, { children: "No assignees" }) : issue.assignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25940
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChipRow, { children: issue.assignees.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Empty, { children: "No assignees" }) : issue.assignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25684
25941
  Button,
25685
25942
  {
25686
25943
  variant: "chip",
@@ -25697,7 +25954,7 @@ var IssueDetails = ({
25697
25954
  },
25698
25955
  assignee.id
25699
25956
  )) }),
25700
- addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChipRow, { children: availableAssignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25957
+ addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChipRow, { children: availableAssignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25701
25958
  Button,
25702
25959
  {
25703
25960
  variant: "chip",
@@ -25712,8 +25969,8 @@ var IssueDetails = ({
25712
25969
  },
25713
25970
  assignee.id
25714
25971
  )) }),
25715
- addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(AddRow, { children: [
25716
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25972
+ addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(AddRow, { children: [
25973
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25717
25974
  Input,
25718
25975
  {
25719
25976
  value: assigneeName,
@@ -25729,21 +25986,32 @@ var IssueDetails = ({
25729
25986
  }
25730
25987
  }
25731
25988
  ),
25732
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { onClick: addAssignee, children: "add" })
25989
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: addAssignee, children: "add" })
25733
25990
  ] })
25734
25991
  ]
25735
25992
  }
25736
25993
  ),
25737
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25994
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25995
+ IssueAttachments,
25996
+ {
25997
+ issueId: issue.id,
25998
+ readonly: Boolean(issue.readonly),
25999
+ attachments,
26000
+ uploadStatus: attachmentUploadStatus,
26001
+ onUploadFiles: onUploadAttachments,
26002
+ onDeleteAttachment
26003
+ }
26004
+ ),
26005
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25738
26006
  Section,
25739
26007
  {
25740
26008
  title: "Actions",
25741
- action: issue.isClosed ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { onClick: () => onReopenIssue(issue.id), children: "reopen issue" }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { onClick: () => onCloseIssue(issue.id), children: "close issue" }),
26009
+ action: issue.isClosed ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: () => onReopenIssue(issue.id), children: "reopen issue" }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: () => onCloseIssue(issue.id), children: "close issue" }),
25742
26010
  children: ""
25743
26011
  }
25744
26012
  )
25745
26013
  ] }),
25746
- activeTab === "comments" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
26014
+ activeTab === "comments" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25747
26015
  IssueComments,
25748
26016
  {
25749
26017
  whoAmI,
@@ -25754,15 +26022,15 @@ var IssueDetails = ({
25754
26022
  onDeleteComment
25755
26023
  }
25756
26024
  )
25757
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Empty, { children: "Select an issue" }) });
26025
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Empty, { children: "Select an issue" }) });
25758
26026
  };
25759
26027
 
25760
26028
  // source/gui/client/components/SwimlaneColumn.tsx
25761
- var import_react7 = __toESM(require_react(), 1);
26029
+ var import_react8 = __toESM(require_react(), 1);
25762
26030
 
25763
26031
  // source/gui/client/components/IconComment.tsx
25764
- var import_jsx_runtime15 = __toESM(require_jsx_runtime(), 1);
25765
- var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26032
+ var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
26033
+ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
25766
26034
  "svg",
25767
26035
  {
25768
26036
  width: size,
@@ -25770,7 +26038,7 @@ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime15.js
25770
26038
  viewBox: "0 0 16 16",
25771
26039
  fill: "none",
25772
26040
  "aria-hidden": "true",
25773
- children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26041
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
25774
26042
  "path",
25775
26043
  {
25776
26044
  d: "M3.9 2.6h5.2a2.4 2.4 0 0 1 2.4 2.4v2.5a2.4 2.4 0 0 1-2.4 2.4H6.6l-1.8 2.1.2-2.1H3.9a2.4 2.4 0 0 1-2.4-2.4V5a2.4 2.4 0 0 1 2.4-2.4Z",
@@ -25785,7 +26053,7 @@ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime15.js
25785
26053
  );
25786
26054
 
25787
26055
  // source/gui/client/components/TicketCard.tsx
25788
- var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
26056
+ var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
25789
26057
  var TicketCard = ({
25790
26058
  ticket,
25791
26059
  index,
@@ -25798,7 +26066,7 @@ var TicketCard = ({
25798
26066
  onDropIssueAt
25799
26067
  }) => {
25800
26068
  const getVisualTargetIndex = (isAfterMiddle) => index + (isAfterMiddle ? 1 : 0);
25801
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
26069
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25802
26070
  "div",
25803
26071
  {
25804
26072
  draggable: !ticket.readonly,
@@ -25839,7 +26107,7 @@ var TicketCard = ({
25839
26107
  border: `1px solid ${isSelected ? GUI_THEME.accent : "transparent"}`
25840
26108
  },
25841
26109
  children: [
25842
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26110
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25843
26111
  "div",
25844
26112
  {
25845
26113
  style: {
@@ -25852,7 +26120,7 @@ var TicketCard = ({
25852
26120
  children: isSelected ? "\u276F" : index + 1
25853
26121
  }
25854
26122
  ),
25855
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
26123
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25856
26124
  "div",
25857
26125
  {
25858
26126
  style: {
@@ -25863,7 +26131,7 @@ var TicketCard = ({
25863
26131
  gap: 12
25864
26132
  },
25865
26133
  children: [
25866
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
26134
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25867
26135
  "div",
25868
26136
  {
25869
26137
  style: {
@@ -25874,7 +26142,7 @@ var TicketCard = ({
25874
26142
  flex: 1
25875
26143
  },
25876
26144
  children: [
25877
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26145
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25878
26146
  "div",
25879
26147
  {
25880
26148
  style: {
@@ -25884,10 +26152,10 @@ var TicketCard = ({
25884
26152
  fontWeight: 500,
25885
26153
  fontSize: 12
25886
26154
  },
25887
- children: ticket.ref && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { style: { marginRight: 6 }, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(CopyRef, { refValue: ticket.ref }) })
26155
+ children: ticket.ref && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { style: { marginRight: 6 }, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CopyRef, { refValue: ticket.ref }) })
25888
26156
  }
25889
26157
  ),
25890
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26158
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25891
26159
  "div",
25892
26160
  {
25893
26161
  style: {
@@ -25900,7 +26168,7 @@ var TicketCard = ({
25900
26168
  children: ticket.title
25901
26169
  }
25902
26170
  ),
25903
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26171
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25904
26172
  "div",
25905
26173
  {
25906
26174
  style: {
@@ -25909,7 +26177,7 @@ var TicketCard = ({
25909
26177
  gap: 6,
25910
26178
  alignItems: "center"
25911
26179
  },
25912
- children: ticket.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26180
+ children: ticket.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25913
26181
  "span",
25914
26182
  {
25915
26183
  style: {
@@ -25929,7 +26197,7 @@ var TicketCard = ({
25929
26197
  ]
25930
26198
  }
25931
26199
  ),
25932
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26200
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25933
26201
  "div",
25934
26202
  {
25935
26203
  style: {
@@ -25939,7 +26207,7 @@ var TicketCard = ({
25939
26207
  flexShrink: 0,
25940
26208
  paddingTop: 2
25941
26209
  },
25942
- children: commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
26210
+ children: commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25943
26211
  "button",
25944
26212
  {
25945
26213
  type: "button",
@@ -25977,14 +26245,14 @@ var TicketCard = ({
25977
26245
  event.currentTarget.style.transform = "translateY(0)";
25978
26246
  },
25979
26247
  children: [
25980
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(IconComment, {}),
25981
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: commentCount })
26248
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(IconComment, {}),
26249
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: commentCount })
25982
26250
  ]
25983
26251
  }
25984
26252
  )
25985
26253
  }
25986
26254
  ),
25987
- ticket.assignees.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26255
+ ticket.assignees.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25988
26256
  "div",
25989
26257
  {
25990
26258
  style: {
@@ -25994,7 +26262,7 @@ var TicketCard = ({
25994
26262
  paddingTop: 2,
25995
26263
  marginTop: "-4px"
25996
26264
  },
25997
- children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(User, { user: assignee, index: idx, isFocus: isSelected }))
26265
+ children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(User, { user: assignee, index: idx, isFocus: isSelected }))
25998
26266
  }
25999
26267
  )
26000
26268
  ]
@@ -26006,7 +26274,7 @@ var TicketCard = ({
26006
26274
  };
26007
26275
 
26008
26276
  // source/gui/client/components/SwimlaneColumn.tsx
26009
- var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
26277
+ var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
26010
26278
  var SwimlaneColumn = ({
26011
26279
  swimlane,
26012
26280
  selected,
@@ -26022,7 +26290,7 @@ var SwimlaneColumn = ({
26022
26290
  onDragOverIssue,
26023
26291
  onDragLeave
26024
26292
  }) => {
26025
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
26293
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
26026
26294
  Panel,
26027
26295
  {
26028
26296
  as: "section",
@@ -26062,7 +26330,7 @@ var SwimlaneColumn = ({
26062
26330
  onDragLeave();
26063
26331
  },
26064
26332
  children: [
26065
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
26333
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
26066
26334
  "header",
26067
26335
  {
26068
26336
  style: {
@@ -26073,7 +26341,7 @@ var SwimlaneColumn = ({
26073
26341
  alignItems: "center"
26074
26342
  },
26075
26343
  children: [
26076
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
26344
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
26077
26345
  "div",
26078
26346
  {
26079
26347
  style: {
@@ -26082,30 +26350,30 @@ var SwimlaneColumn = ({
26082
26350
  gap: 8
26083
26351
  },
26084
26352
  children: [
26085
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
26353
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26086
26354
  "span",
26087
26355
  {
26088
26356
  style: { color: selected ? GUI_THEME.accent : GUI_THEME.secondary },
26089
26357
  children: selected ? "\u276F" : " "
26090
26358
  }
26091
26359
  ),
26092
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
26360
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26093
26361
  "strong",
26094
26362
  {
26095
26363
  style: { color: selected ? GUI_THEME.accent : GUI_THEME.primary },
26096
26364
  children: swimlane.title
26097
26365
  }
26098
26366
  ),
26099
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { style: { color: GUI_THEME.dim }, children: [
26367
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { style: { color: GUI_THEME.dim }, children: [
26100
26368
  "(",
26101
26369
  swimlane.issues.length,
26102
26370
  ")"
26103
26371
  ] }),
26104
- swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: "\u{1F512}" })
26372
+ swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "\u{1F512}" })
26105
26373
  ]
26106
26374
  }
26107
26375
  ),
26108
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
26376
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26109
26377
  Button,
26110
26378
  {
26111
26379
  variant: "ghost",
@@ -26118,9 +26386,9 @@ var SwimlaneColumn = ({
26118
26386
  ]
26119
26387
  }
26120
26388
  ),
26121
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: { overflow: "auto", paddingTop: 4, flex: 1 }, children: swimlane.issues.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
26122
- dropIndex === 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DropIndicator, {}),
26123
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
26389
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { overflow: "auto", paddingTop: 4, flex: 1 }, children: swimlane.issues.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
26390
+ dropIndex === 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(DropIndicator, {}),
26391
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26124
26392
  "div",
26125
26393
  {
26126
26394
  style: {
@@ -26132,10 +26400,10 @@ var SwimlaneColumn = ({
26132
26400
  children: "Drop issue here"
26133
26401
  }
26134
26402
  )
26135
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
26136
- swimlane.issues.map((ticket, index) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_react7.default.Fragment, { children: [
26137
- dropIndex === index && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DropIndicator, {}),
26138
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
26403
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
26404
+ swimlane.issues.map((ticket, index) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react8.default.Fragment, { children: [
26405
+ dropIndex === index && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(DropIndicator, {}),
26406
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26139
26407
  TicketCard,
26140
26408
  {
26141
26409
  ticket,
@@ -26153,7 +26421,7 @@ var SwimlaneColumn = ({
26153
26421
  }
26154
26422
  )
26155
26423
  ] }, ticket.id)),
26156
- dropIndex === swimlane.issues.length && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DropIndicator, {})
26424
+ dropIndex === swimlane.issues.length && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(DropIndicator, {})
26157
26425
  ] }) })
26158
26426
  ]
26159
26427
  }
@@ -26291,9 +26559,73 @@ var updateIssueInGuiState = (state, issueId, updateIssue) => ({
26291
26559
  }))
26292
26560
  });
26293
26561
 
26562
+ // source/gui/client/lib/compress-image.ts
26563
+ var MAX_LONG_EDGE = 1600;
26564
+ var DEFAULT_MAX_KB = 500;
26565
+ var QUALITY_STEPS = [0.8, 0.7, 0.6, 0.5, 0.4];
26566
+ var toBlob = (canvas, type, quality) => new Promise((resolve) => canvas.toBlob(resolve, type, quality));
26567
+ var replaceExtension = (name, ext) => {
26568
+ const base = name.replace(/\.[a-z0-9]+$/i, "");
26569
+ return `${base || "image"}.${ext}`;
26570
+ };
26571
+ var compressImage = async (file, maxKb = DEFAULT_MAX_KB) => {
26572
+ const maxBytes = maxKb * 1024;
26573
+ if (file.type === "image/gif") {
26574
+ if (file.size <= maxBytes) {
26575
+ return { blob: file, name: file.name };
26576
+ }
26577
+ return {
26578
+ error: `GIFs are attached as-is and this one exceeds ${maxKb} KB`
26579
+ };
26580
+ }
26581
+ let bitmap;
26582
+ try {
26583
+ bitmap = await createImageBitmap(file);
26584
+ } catch {
26585
+ return { error: "Not a supported image" };
26586
+ }
26587
+ const scale = Math.min(
26588
+ 1,
26589
+ MAX_LONG_EDGE / Math.max(bitmap.width, bitmap.height)
26590
+ );
26591
+ const canvas = document.createElement("canvas");
26592
+ canvas.width = Math.max(1, Math.round(bitmap.width * scale));
26593
+ canvas.height = Math.max(1, Math.round(bitmap.height * scale));
26594
+ const context = canvas.getContext("2d");
26595
+ if (!context) {
26596
+ bitmap.close();
26597
+ return { error: "Unable to process image" };
26598
+ }
26599
+ context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
26600
+ bitmap.close();
26601
+ for (const quality of QUALITY_STEPS) {
26602
+ const blob = await toBlob(canvas, "image/webp", quality);
26603
+ if (blob && blob.size <= maxBytes) {
26604
+ return { blob, name: replaceExtension(file.name, "webp") };
26605
+ }
26606
+ }
26607
+ return {
26608
+ error: `Unable to compress image under ${maxKb} KB`
26609
+ };
26610
+ };
26611
+ var blobToBase64 = (blob) => new Promise((resolve, reject) => {
26612
+ const reader = new FileReader();
26613
+ reader.onload = () => {
26614
+ const dataUrl = String(reader.result ?? "");
26615
+ const base64 = dataUrl.split(",")[1];
26616
+ if (!base64) {
26617
+ reject(new Error("Unable to encode image"));
26618
+ return;
26619
+ }
26620
+ resolve(base64);
26621
+ };
26622
+ reader.onerror = () => reject(new Error("Unable to read image"));
26623
+ reader.readAsDataURL(blob);
26624
+ });
26625
+
26294
26626
  // source/gui/client/App.tsx
26295
- var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
26296
- var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26627
+ var import_jsx_runtime19 = __toESM(require_jsx_runtime(), 1);
26628
+ var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26297
26629
  "div",
26298
26630
  {
26299
26631
  style: {
@@ -26308,34 +26640,51 @@ var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26308
26640
  var App = () => {
26309
26641
  const { boardId, issueId } = useParams();
26310
26642
  const [searchParams, setSearchParams] = useSearchParams();
26311
- const [connected, setConnected] = (0, import_react8.useState)(false);
26312
- const [syncStatus, setSyncStatus] = (0, import_react8.useState)({
26643
+ const [connected, setConnected] = (0, import_react9.useState)(false);
26644
+ const [syncStatus, setSyncStatus] = (0, import_react9.useState)({
26313
26645
  status: "synced",
26314
26646
  msg: "idle"
26315
26647
  });
26316
- const [state, setState] = (0, import_react8.useState)(null);
26317
- const [dragOverSwimlaneId, setDragOverSwimlaneId] = (0, import_react8.useState)(
26648
+ const [state, setState] = (0, import_react9.useState)(null);
26649
+ const [dragOverSwimlaneId, setDragOverSwimlaneId] = (0, import_react9.useState)(
26318
26650
  null
26319
26651
  );
26320
- const [dropTarget, setDropTarget] = (0, import_react8.useState)(null);
26321
- const [boardMenuOpen, setBoardMenuOpen] = (0, import_react8.useState)(false);
26322
- const [createIssueModal, setCreateIssueModal] = (0, import_react8.useState)(null);
26323
- const boardMenuRef = (0, import_react8.useRef)(null);
26324
- const socketRef = (0, import_react8.useRef)(null);
26652
+ const [dropTarget, setDropTarget] = (0, import_react9.useState)(null);
26653
+ const [boardMenuOpen, setBoardMenuOpen] = (0, import_react9.useState)(false);
26654
+ const [createIssueModal, setCreateIssueModal] = (0, import_react9.useState)(null);
26655
+ const boardMenuRef = (0, import_react9.useRef)(null);
26656
+ const socketRef = (0, import_react9.useRef)(null);
26325
26657
  const selectedTab = searchParams.get("tab") === "comments" ? "comments" : "overview";
26326
26658
  const navigate = useNavigate();
26327
26659
  const selectedBoard = (state && boardId ? findBoard(state, boardId) : null) ?? state?.boards[0] ?? null;
26328
26660
  const boardSlug = selectedBoard?.ref ?? boardId;
26329
26661
  const selectedIssue = state && issueId ? findIssue(state, issueId) : null;
26662
+ (0, import_react9.useEffect)(() => {
26663
+ const onPaste = (event) => {
26664
+ if (!selectedIssue || selectedIssue.readonly) return;
26665
+ const target = event.target;
26666
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) {
26667
+ return;
26668
+ }
26669
+ const files = Array.from(event.clipboardData?.items ?? []).filter((item) => item.kind === "file" && item.type.startsWith("image/")).map((item) => item.getAsFile()).filter((file) => Boolean(file));
26670
+ if (files.length === 0) return;
26671
+ event.preventDefault();
26672
+ void uploadIssueAttachments(selectedIssue.id, files);
26673
+ };
26674
+ window.addEventListener("paste", onPaste);
26675
+ return () => window.removeEventListener("paste", onPaste);
26676
+ });
26330
26677
  const closeIssueDetails = () => {
26331
26678
  if (!boardSlug) return;
26332
26679
  void navigate(`/board/${boardSlug}`);
26333
26680
  };
26334
26681
  const commentsByIssueId = state?.commentsByIssueId ?? {};
26682
+ const attachmentsByIssueId = state?.attachmentsByIssueId ?? {};
26683
+ const [attachmentUploadStatus, setAttachmentUploadStatus] = (0, import_react9.useState)({ state: "idle" });
26335
26684
  const requestState = () => {
26336
26685
  socketRef.current?.send(JSON.stringify({ type: "state:get" }));
26337
26686
  };
26338
- (0, import_react8.useEffect)(() => {
26687
+ (0, import_react9.useEffect)(() => {
26339
26688
  const socket = new WebSocket(
26340
26689
  `ws://${window.location.host}/ws${boardId ? `?boardId=${boardId}` : ""}`
26341
26690
  );
@@ -26379,12 +26728,12 @@ var App = () => {
26379
26728
  socket.close();
26380
26729
  };
26381
26730
  }, [boardId, navigate]);
26382
- (0, import_react8.useEffect)(() => {
26731
+ (0, import_react9.useEffect)(() => {
26383
26732
  if (!boardId && state?.boards[0]) {
26384
26733
  void navigate(`/board/${state.boards[0].ref}`, { replace: true });
26385
26734
  }
26386
26735
  }, [boardId, state, navigate]);
26387
- (0, import_react8.useEffect)(() => {
26736
+ (0, import_react9.useEffect)(() => {
26388
26737
  const close = (event) => {
26389
26738
  if (boardMenuRef.current && !boardMenuRef.current.contains(event.target)) {
26390
26739
  setBoardMenuOpen(false);
@@ -26569,6 +26918,68 @@ var App = () => {
26569
26918
  });
26570
26919
  send("issue:comment:add", { issueId: issueId2, body });
26571
26920
  };
26921
+ const uploadIssueAttachments = async (issueId2, files) => {
26922
+ for (const file of files) {
26923
+ setAttachmentUploadStatus({ state: "uploading", name: file.name });
26924
+ const compressed = await compressImage(file, state?.attachmentMaxKb);
26925
+ if ("error" in compressed) {
26926
+ setAttachmentUploadStatus({ state: "error", message: compressed.error });
26927
+ return;
26928
+ }
26929
+ try {
26930
+ const dataBase64 = await blobToBase64(compressed.blob);
26931
+ const response = await fetch("/api/attachments", {
26932
+ method: "POST",
26933
+ headers: { "content-type": "application/json" },
26934
+ body: JSON.stringify({
26935
+ issueId: issueId2,
26936
+ name: compressed.name,
26937
+ dataBase64
26938
+ })
26939
+ });
26940
+ const payload = await response.json();
26941
+ if (!response.ok) {
26942
+ setAttachmentUploadStatus({
26943
+ state: "error",
26944
+ message: payload?.message ?? "Upload failed"
26945
+ });
26946
+ return;
26947
+ }
26948
+ const nextState = getResultValue(payload);
26949
+ if (nextState) setState(nextState);
26950
+ } catch (error) {
26951
+ setAttachmentUploadStatus({
26952
+ state: "error",
26953
+ message: error instanceof Error ? error.message : "Upload failed"
26954
+ });
26955
+ return;
26956
+ }
26957
+ }
26958
+ setAttachmentUploadStatus({ state: "idle" });
26959
+ };
26960
+ const deleteIssueAttachment = async (_issueId, attachmentId) => {
26961
+ try {
26962
+ const response = await fetch(
26963
+ `/api/attachments/${encodeURIComponent(attachmentId)}`,
26964
+ { method: "DELETE" }
26965
+ );
26966
+ const payload = await response.json();
26967
+ if (!response.ok) {
26968
+ setAttachmentUploadStatus({
26969
+ state: "error",
26970
+ message: payload?.message ?? "Unable to delete attachment"
26971
+ });
26972
+ return;
26973
+ }
26974
+ const nextState = getResultValue(payload);
26975
+ if (nextState) setState(nextState);
26976
+ } catch (error) {
26977
+ setAttachmentUploadStatus({
26978
+ state: "error",
26979
+ message: error instanceof Error ? error.message : "Unable to delete attachment"
26980
+ });
26981
+ }
26982
+ };
26572
26983
  const deleteIssueComment = (_issueId, commentId) => {
26573
26984
  setState((prev) => {
26574
26985
  if (!prev) return prev;
@@ -26586,7 +26997,7 @@ var App = () => {
26586
26997
  });
26587
26998
  send("issue:comment:delete", { commentId });
26588
26999
  };
26589
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
27000
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
26590
27001
  "div",
26591
27002
  {
26592
27003
  style: {
@@ -26598,8 +27009,8 @@ var App = () => {
26598
27009
  flexDirection: "column"
26599
27010
  },
26600
27011
  children: [
26601
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Header, { state, connected, syncStatus }),
26602
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
27012
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Header, { state, connected, syncStatus }),
27013
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
26603
27014
  "div",
26604
27015
  {
26605
27016
  style: {
@@ -26608,8 +27019,8 @@ var App = () => {
26608
27019
  overflow: "hidden"
26609
27020
  },
26610
27021
  children: [
26611
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("main", { style: { padding: "0 30px 30px 30px", overflow: "auto", flex: 1 }, children: [
26612
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { padding: "20px 10px" }, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
27022
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("main", { style: { padding: "0 30px 30px 30px", overflow: "auto", flex: 1 }, children: [
27023
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { style: { padding: "20px 10px" }, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26613
27024
  Dropdown,
26614
27025
  {
26615
27026
  label: "Board:",
@@ -26625,7 +27036,7 @@ var App = () => {
26625
27036
  onSelect: selectBoard
26626
27037
  }
26627
27038
  ) }),
26628
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { display: "flex", gap: 8 }, children: selectedBoard?.swimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
27039
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { style: { display: "flex", gap: 8 }, children: selectedBoard?.swimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26629
27040
  SwimlaneColumn,
26630
27041
  {
26631
27042
  swimlane,
@@ -26645,7 +27056,7 @@ var App = () => {
26645
27056
  swimlane.id
26646
27057
  )) })
26647
27058
  ] }),
26648
- selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
27059
+ selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26649
27060
  IssueDetails,
26650
27061
  {
26651
27062
  whoAmI: state.user,
@@ -26662,6 +27073,10 @@ var App = () => {
26662
27073
  onRemoveAssignee: removeIssueAssignee,
26663
27074
  onAddComment: addIssueComment,
26664
27075
  onDeleteComment: deleteIssueComment,
27076
+ attachments: attachmentsByIssueId[selectedIssue.id] ?? [],
27077
+ attachmentUploadStatus,
27078
+ onUploadAttachments: uploadIssueAttachments,
27079
+ onDeleteAttachment: deleteIssueAttachment,
26665
27080
  onReopenIssue: reopenIssue,
26666
27081
  onCloseIssue: closeIssue,
26667
27082
  knownTags: state.tags ?? [],
@@ -26671,7 +27086,7 @@ var App = () => {
26671
27086
  ]
26672
27087
  }
26673
27088
  ),
26674
- createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
27089
+ createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26675
27090
  CreateIssueModal,
26676
27091
  {
26677
27092
  title: createIssueModal.title,
@@ -26686,14 +27101,14 @@ var App = () => {
26686
27101
  };
26687
27102
 
26688
27103
  // source/gui/client/main.tsx
26689
- var import_jsx_runtime19 = __toESM(require_jsx_runtime(), 1);
27104
+ var import_jsx_runtime20 = __toESM(require_jsx_runtime(), 1);
26690
27105
  (0, import_client.createRoot)(document.getElementById("root")).render(
26691
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(BrowserRouter, { children: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(Routes, { children: [
26692
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Route, { path: "/", element: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(App, {}) }),
26693
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Route, { path: "/board/:boardId", element: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(App, {}) }),
26694
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Route, { path: "/board/:boardId/issue/:issueId", element: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(App, {}) }),
26695
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Route, { path: "/board/:boardId/:issueId", element: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(App, {}) }),
26696
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Route, { path: "*", element: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Navigate, { to: "/", replace: true }) })
27106
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(BrowserRouter, { children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(Routes, { children: [
27107
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Route, { path: "/", element: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(App, {}) }),
27108
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Route, { path: "/board/:boardId", element: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(App, {}) }),
27109
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Route, { path: "/board/:boardId/issue/:issueId", element: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(App, {}) }),
27110
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Route, { path: "/board/:boardId/:issueId", element: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(App, {}) }),
27111
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Route, { path: "*", element: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Navigate, { to: "/", replace: true }) })
26697
27112
  ] }) })
26698
27113
  );
26699
27114
  /*! Bundled license information: