epiq 1.2.3 → 1.3.1

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);
@@ -24871,7 +24871,10 @@ var Panel = ({
24871
24871
  style: {
24872
24872
  position: "relative",
24873
24873
  zIndex: 1,
24874
- height: "100%"
24874
+ height: "100%",
24875
+ display: "flex",
24876
+ flexDirection: "column",
24877
+ minHeight: 0
24875
24878
  },
24876
24879
  children
24877
24880
  }
@@ -24911,12 +24914,13 @@ var User = ({
24911
24914
  );
24912
24915
 
24913
24916
  // source/version.ts
24914
- var EPIQ_VERSION = "1.2.3";
24917
+ var EPIQ_VERSION = "1.3.1";
24915
24918
 
24916
24919
  // source/gui/client/components/Header.tsx
24917
24920
  var import_jsx_runtime6 = __toESM(require_jsx_runtime(), 1);
24918
24921
  var Header = ({ state, connected, syncStatus }) => {
24919
24922
  const syncColor = syncStatus.status === "synced" ? GUI_THEME.green : syncStatus.status === "failed" ? GUI_THEME.red : GUI_THEME.accent;
24923
+ const syncLabel = syncStatus.status === "failed" ? "-" : syncStatus.msg.toLowerCase();
24920
24924
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
24921
24925
  Panel,
24922
24926
  {
@@ -24986,12 +24990,17 @@ var Header = ({ state, connected, syncStatus }) => {
24986
24990
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
24987
24991
  "span",
24988
24992
  {
24993
+ title: syncStatus.status === "failed" ? syncStatus.msg : void 0,
24989
24994
  style: {
24990
24995
  color: GUI_THEME.dim,
24991
24996
  minWidth: 72,
24997
+ maxWidth: 220,
24998
+ overflow: "hidden",
24999
+ textOverflow: "ellipsis",
25000
+ whiteSpace: "nowrap",
24992
25001
  textAlign: "right"
24993
25002
  },
24994
- children: connected ? syncStatus.msg.toLowerCase() : "-"
25003
+ children: connected ? syncLabel : "-"
24995
25004
  }
24996
25005
  ),
24997
25006
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -25042,7 +25051,7 @@ var Header = ({ state, connected, syncStatus }) => {
25042
25051
  };
25043
25052
 
25044
25053
  // source/gui/client/components/IssueDetails.tsx
25045
- var import_react6 = __toESM(require_react(), 1);
25054
+ var import_react7 = __toESM(require_react(), 1);
25046
25055
 
25047
25056
  // source/gui/client/components/Aside.tsx
25048
25057
  var import_jsx_runtime7 = __toESM(require_jsx_runtime(), 1);
@@ -25108,7 +25117,7 @@ var CopyRef = ({ refValue }) => {
25108
25117
  onMouseLeave: (event) => {
25109
25118
  event.currentTarget.style.color = copied ? GUI_THEME.green : GUI_THEME.dim;
25110
25119
  },
25111
- children: copied ? "copied!" : `#${refValue}`
25120
+ children: copied ? "copied!" : refValue
25112
25121
  }
25113
25122
  );
25114
25123
  };
@@ -25182,9 +25191,299 @@ var AddRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("di
25182
25191
  var ChipRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10 }, children });
25183
25192
  var Empty = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { display: "inline-block", marginTop: 8, color: GUI_THEME.dim }, children });
25184
25193
 
25185
- // source/gui/client/components/IssueComments.tsx
25194
+ // source/gui/client/components/IssueAttachments.tsx
25186
25195
  var import_react5 = __toESM(require_react(), 1);
25187
25196
 
25197
+ // source/gui/client/components/Section.tsx
25198
+ var import_jsx_runtime11 = __toESM(require_jsx_runtime(), 1);
25199
+ var Section = ({
25200
+ title,
25201
+ action,
25202
+ children,
25203
+ first = false
25204
+ }) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
25205
+ "section",
25206
+ {
25207
+ style: {
25208
+ padding: first ? "0px 0 14px 0" : "14px 0",
25209
+ borderTop: first ? "none" : `1px solid ${GUI_THEME.line}`
25210
+ },
25211
+ children: [
25212
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
25213
+ "div",
25214
+ {
25215
+ style: {
25216
+ display: "flex",
25217
+ justifyContent: "space-between",
25218
+ alignItems: "center",
25219
+ gap: 12
25220
+ },
25221
+ children: [
25222
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
25223
+ "span",
25224
+ {
25225
+ style: {
25226
+ color: GUI_THEME.secondary,
25227
+ fontSize: 10,
25228
+ textTransform: "uppercase",
25229
+ letterSpacing: "0.08em"
25230
+ },
25231
+ children: title
25232
+ }
25233
+ ),
25234
+ action
25235
+ ]
25236
+ }
25237
+ ),
25238
+ children
25239
+ ]
25240
+ }
25241
+ );
25242
+
25243
+ // source/gui/client/components/IssueAttachments.tsx
25244
+ var import_jsx_runtime12 = __toESM(require_jsx_runtime(), 1);
25245
+ var IssueAttachments = ({
25246
+ issueId,
25247
+ readonly,
25248
+ attachments,
25249
+ uploadStatus,
25250
+ onUploadFiles,
25251
+ onDeleteAttachment
25252
+ }) => {
25253
+ const [dragging, setDragging] = (0, import_react5.useState)(false);
25254
+ const [lightbox, setLightbox] = (0, import_react5.useState)(null);
25255
+ const [broken, setBroken] = (0, import_react5.useState)({});
25256
+ (0, import_react5.useEffect)(() => {
25257
+ setLightbox(null);
25258
+ setDragging(false);
25259
+ setBroken({});
25260
+ }, [issueId]);
25261
+ (0, import_react5.useEffect)(() => {
25262
+ if (!lightbox) return;
25263
+ const onKeyDown = (event) => {
25264
+ if (event.key === "Escape") setLightbox(null);
25265
+ };
25266
+ window.addEventListener("keydown", onKeyDown);
25267
+ return () => window.removeEventListener("keydown", onKeyDown);
25268
+ }, [lightbox]);
25269
+ const canUpload = !readonly && Boolean(onUploadFiles);
25270
+ const handleDrop = (event) => {
25271
+ event.preventDefault();
25272
+ setDragging(false);
25273
+ if (!canUpload) return;
25274
+ const files = Array.from(event.dataTransfer.files).filter(
25275
+ (file) => file.type.startsWith("image/")
25276
+ );
25277
+ if (files.length > 0) onUploadFiles?.(issueId, files);
25278
+ };
25279
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25280
+ Section,
25281
+ {
25282
+ title: `Attachments${attachments.length ? ` (${attachments.length})` : ""}`,
25283
+ children: [
25284
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25285
+ "div",
25286
+ {
25287
+ onDragOver: (event) => {
25288
+ if (!canUpload) return;
25289
+ event.preventDefault();
25290
+ setDragging(true);
25291
+ },
25292
+ onDragLeave: () => setDragging(false),
25293
+ onDrop: handleDrop,
25294
+ style: {
25295
+ marginTop: 8,
25296
+ padding: dragging ? 10 : 0,
25297
+ border: dragging ? `1px dashed ${GUI_THEME.accent}` : "1px dashed transparent",
25298
+ borderRadius: 4,
25299
+ transition: "padding 0.12s ease, border-color 0.12s ease"
25300
+ },
25301
+ children: [
25302
+ 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)(
25303
+ "div",
25304
+ {
25305
+ style: {
25306
+ display: "flex",
25307
+ flexWrap: "wrap",
25308
+ gap: 8
25309
+ },
25310
+ children: attachments.map((attachment) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25311
+ "div",
25312
+ {
25313
+ style: { position: "relative", width: 72, height: 72 },
25314
+ children: [
25315
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25316
+ "button",
25317
+ {
25318
+ type: "button",
25319
+ disabled: broken[attachment.id],
25320
+ onClick: () => setLightbox(attachment),
25321
+ title: `${attachment.name} (${Math.max(
25322
+ 1,
25323
+ Math.round(attachment.bytes / 1024)
25324
+ )} KB)`,
25325
+ style: {
25326
+ padding: 0,
25327
+ border: `1px solid ${GUI_THEME.line}`,
25328
+ borderRadius: 4,
25329
+ background: GUI_THEME.panel2,
25330
+ cursor: broken[attachment.id] ? "default" : "zoom-in",
25331
+ overflow: "hidden",
25332
+ width: "100%",
25333
+ height: "100%"
25334
+ },
25335
+ children: broken[attachment.id] ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25336
+ "span",
25337
+ {
25338
+ title: "Attachment unavailable (missing, corrupt, or over the size cap)",
25339
+ style: {
25340
+ display: "flex",
25341
+ alignItems: "center",
25342
+ justifyContent: "center",
25343
+ width: "100%",
25344
+ height: "100%",
25345
+ color: GUI_THEME.dim2,
25346
+ fontSize: 9,
25347
+ padding: 4,
25348
+ textAlign: "center"
25349
+ },
25350
+ children: "unavailable"
25351
+ }
25352
+ ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25353
+ "img",
25354
+ {
25355
+ src: `/media/${attachment.fileName}`,
25356
+ alt: attachment.name,
25357
+ loading: "lazy",
25358
+ onError: () => setBroken((prev) => ({ ...prev, [attachment.id]: true })),
25359
+ style: {
25360
+ width: "100%",
25361
+ height: "100%",
25362
+ objectFit: "cover",
25363
+ display: "block"
25364
+ }
25365
+ }
25366
+ )
25367
+ }
25368
+ ),
25369
+ attachment.canDelete && !readonly && onDeleteAttachment && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25370
+ "button",
25371
+ {
25372
+ type: "button",
25373
+ title: "Delete attachment",
25374
+ onClick: () => onDeleteAttachment(issueId, attachment.id),
25375
+ style: {
25376
+ position: "absolute",
25377
+ top: 2,
25378
+ right: 2,
25379
+ width: 16,
25380
+ height: 16,
25381
+ lineHeight: "14px",
25382
+ padding: 0,
25383
+ border: `1px solid ${GUI_THEME.line}`,
25384
+ borderRadius: 3,
25385
+ background: "rgba(4, 5, 8, 0.75)",
25386
+ color: GUI_THEME.secondary,
25387
+ fontSize: 10,
25388
+ cursor: "pointer"
25389
+ },
25390
+ children: "\xD7"
25391
+ }
25392
+ )
25393
+ ]
25394
+ },
25395
+ attachment.id
25396
+ ))
25397
+ }
25398
+ ),
25399
+ canUpload && attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25400
+ "div",
25401
+ {
25402
+ style: {
25403
+ marginTop: 8,
25404
+ color: GUI_THEME.dim2,
25405
+ fontSize: 10
25406
+ },
25407
+ children: "drop an image to attach"
25408
+ }
25409
+ ),
25410
+ uploadStatus.state === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25411
+ "div",
25412
+ {
25413
+ style: {
25414
+ marginTop: 8,
25415
+ color: GUI_THEME.secondary,
25416
+ fontSize: 11
25417
+ },
25418
+ children: [
25419
+ "uploading ",
25420
+ uploadStatus.name,
25421
+ "\u2026"
25422
+ ]
25423
+ }
25424
+ ),
25425
+ uploadStatus.state === "error" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25426
+ "div",
25427
+ {
25428
+ style: {
25429
+ marginTop: 8,
25430
+ color: GUI_THEME.red,
25431
+ fontSize: 11
25432
+ },
25433
+ children: uploadStatus.message
25434
+ }
25435
+ )
25436
+ ]
25437
+ }
25438
+ ),
25439
+ lightbox && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25440
+ "div",
25441
+ {
25442
+ onClick: () => setLightbox(null),
25443
+ style: {
25444
+ position: "fixed",
25445
+ inset: 0,
25446
+ zIndex: 100,
25447
+ display: "flex",
25448
+ alignItems: "center",
25449
+ justifyContent: "center",
25450
+ flexDirection: "column",
25451
+ gap: 10,
25452
+ background: "rgba(4, 5, 8, 0.88)",
25453
+ cursor: "zoom-out"
25454
+ },
25455
+ children: [
25456
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25457
+ "img",
25458
+ {
25459
+ src: `/media/${lightbox.fileName}`,
25460
+ alt: lightbox.name,
25461
+ style: {
25462
+ maxWidth: "90vw",
25463
+ maxHeight: "85vh",
25464
+ borderRadius: 6,
25465
+ border: `1px solid ${GUI_THEME.line}`
25466
+ }
25467
+ }
25468
+ ),
25469
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 11 }, children: [
25470
+ lightbox.name,
25471
+ " \xB7 ",
25472
+ Math.max(1, Math.round(lightbox.bytes / 1024)),
25473
+ " ",
25474
+ "KB \xB7 esc to close"
25475
+ ] })
25476
+ ]
25477
+ }
25478
+ )
25479
+ ]
25480
+ }
25481
+ );
25482
+ };
25483
+
25484
+ // source/gui/client/components/IssueComments.tsx
25485
+ var import_react6 = __toESM(require_react(), 1);
25486
+
25188
25487
  // source/gui/client/lib/gui-format.helper.ts
25189
25488
  var timeAgo = (timestampMs) => {
25190
25489
  const diff = Date.now() - timestampMs;
@@ -25205,7 +25504,7 @@ var timeAgo = (timestampMs) => {
25205
25504
  };
25206
25505
 
25207
25506
  // source/gui/client/components/IssueComments.tsx
25208
- var import_jsx_runtime11 = __toESM(require_jsx_runtime(), 1);
25507
+ var import_jsx_runtime13 = __toESM(require_jsx_runtime(), 1);
25209
25508
  var IssueComments = ({
25210
25509
  whoAmI,
25211
25510
  issueId,
@@ -25214,15 +25513,15 @@ var IssueComments = ({
25214
25513
  onAddComment,
25215
25514
  onDeleteComment
25216
25515
  }) => {
25217
- const [body, setBody] = (0, import_react5.useState)("");
25516
+ const [body, setBody] = (0, import_react6.useState)("");
25218
25517
  const addComment = () => {
25219
25518
  const nextBody = body.trim();
25220
25519
  if (!nextBody) return;
25221
25520
  onAddComment?.(issueId, nextBody);
25222
25521
  setBody("");
25223
25522
  };
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)(
25523
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { children: [
25524
+ 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
25525
  "div",
25227
25526
  {
25228
25527
  style: {
@@ -25232,7 +25531,7 @@ var IssueComments = ({
25232
25531
  background: GUI_THEME.tertiary
25233
25532
  },
25234
25533
  children: [
25235
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
25534
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25236
25535
  "div",
25237
25536
  {
25238
25537
  style: {
@@ -25242,15 +25541,15 @@ var IssueComments = ({
25242
25541
  marginBottom: 8
25243
25542
  },
25244
25543
  children: [
25245
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 10 }, children: [
25544
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: { color: GUI_THEME.secondary, fontSize: 10 }, children: [
25246
25545
  comment.author.name ?? "unknown",
25247
- comment.createdAt && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { style: { color: GUI_THEME.dim2 }, children: [
25546
+ comment.createdAt && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { style: { color: GUI_THEME.dim2 }, children: [
25248
25547
  " ",
25249
25548
  "\xB7 ",
25250
25549
  timeAgo(comment.createdAt)
25251
25550
  ] })
25252
25551
  ] }),
25253
- comment.author.id === whoAmI.id && onDeleteComment && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
25552
+ comment.author.id === whoAmI.id && onDeleteComment && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25254
25553
  Button,
25255
25554
  {
25256
25555
  variant: "ghost",
@@ -25265,7 +25564,7 @@ var IssueComments = ({
25265
25564
  ]
25266
25565
  }
25267
25566
  ),
25268
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
25567
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25269
25568
  "div",
25270
25569
  {
25271
25570
  style: {
@@ -25280,8 +25579,8 @@ var IssueComments = ({
25280
25579
  },
25281
25580
  comment.id
25282
25581
  )) }),
25283
- !readonly && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { marginTop: 18 }, children: [
25284
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
25582
+ !readonly && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: { marginTop: 18 }, children: [
25583
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25285
25584
  Textarea,
25286
25585
  {
25287
25586
  maxLength: 120,
@@ -25296,64 +25595,18 @@ var IssueComments = ({
25296
25595
  style: { minHeight: 45 }
25297
25596
  }
25298
25597
  ),
25299
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ActionRow, { children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Button, { onClick: addComment, children: "comment" }) })
25598
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ActionRow, { children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { onClick: addComment, children: "comment" }) })
25300
25599
  ] })
25301
25600
  ] });
25302
25601
  };
25303
25602
 
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
25603
  // source/gui/client/components/Tabs.tsx
25351
- var import_jsx_runtime13 = __toESM(require_jsx_runtime(), 1);
25604
+ var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
25352
25605
  var Tabs = ({
25353
25606
  activeTab,
25354
25607
  tabs,
25355
25608
  onChange
25356
- }) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25609
+ }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25357
25610
  "div",
25358
25611
  {
25359
25612
  style: {
@@ -25364,7 +25617,7 @@ var Tabs = ({
25364
25617
  },
25365
25618
  children: tabs.map((tab) => {
25366
25619
  const active = tab.id === activeTab;
25367
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25620
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25368
25621
  "button",
25369
25622
  {
25370
25623
  type: "button",
@@ -25382,7 +25635,7 @@ var Tabs = ({
25382
25635
  },
25383
25636
  children: [
25384
25637
  tab.label,
25385
- typeof tab.count === "number" && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25638
+ typeof tab.count === "number" && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25386
25639
  "span",
25387
25640
  {
25388
25641
  style: { color: tab.count ? GUI_THEME.secondary : GUI_THEME.dim },
@@ -25403,7 +25656,7 @@ var Tabs = ({
25403
25656
  );
25404
25657
 
25405
25658
  // source/gui/client/components/IssueDetails.tsx
25406
- var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
25659
+ var import_jsx_runtime15 = __toESM(require_jsx_runtime(), 1);
25407
25660
  var IssueDetails = ({
25408
25661
  whoAmI,
25409
25662
  comments,
@@ -25421,18 +25674,22 @@ var IssueDetails = ({
25421
25674
  onReopenIssue,
25422
25675
  onAddComment,
25423
25676
  onDeleteComment,
25677
+ attachments,
25678
+ attachmentUploadStatus,
25679
+ onUploadAttachments,
25680
+ onDeleteAttachment,
25424
25681
  knownTags: tags,
25425
25682
  knownAssignees: assignees
25426
25683
  }) => {
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)(() => {
25684
+ const [title, setTitle] = (0, import_react7.useState)("");
25685
+ const [description, setDescription] = (0, import_react7.useState)("");
25686
+ const [tagName, setTagName] = (0, import_react7.useState)("");
25687
+ const [assigneeName, setAssigneeName] = (0, import_react7.useState)("");
25688
+ const [editingTitle, setEditingTitle] = (0, import_react7.useState)(false);
25689
+ const [editingDescription, setEditingDescription] = (0, import_react7.useState)(false);
25690
+ const [addingTag, setAddingTag] = (0, import_react7.useState)(false);
25691
+ const [addingAssignee, setAddingAssignee] = (0, import_react7.useState)(false);
25692
+ (0, import_react7.useEffect)(() => {
25436
25693
  setTitle(issue?.title ?? "");
25437
25694
  setDescription(issue?.description ?? "");
25438
25695
  setTagName("");
@@ -25492,9 +25749,9 @@ var IssueDetails = ({
25492
25749
  const availableAssignees = assignees.filter(
25493
25750
  (assignee) => !issue?.assignees.some((issueAssignee) => issueAssignee.id === assignee.id)
25494
25751
  );
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)(
25752
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Aside, { children: issue ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
25753
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(FormHeader, { children: [
25754
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25498
25755
  "span",
25499
25756
  {
25500
25757
  style: {
@@ -25503,19 +25760,19 @@ var IssueDetails = ({
25503
25760
  textTransform: "uppercase",
25504
25761
  letterSpacing: "0.08em"
25505
25762
  },
25506
- children: issue.ref && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CopyRef, { refValue: issue.ref })
25763
+ children: issue.ref && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CopyRef, { refValue: issue.ref })
25507
25764
  }
25508
25765
  ),
25509
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { variant: "ghost", onClick: onClose, children: "\xD7" })
25766
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "ghost", onClick: onClose, children: "\xD7" })
25510
25767
  ] }),
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)(
25768
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Tabs, { tabs, activeTab, onChange: onChangeTab }),
25769
+ activeTab === "overview" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
25770
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25514
25771
  Section,
25515
25772
  {
25516
25773
  first: true,
25517
25774
  title: "Title",
25518
- action: !issue.readonly && !editingTitle && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25775
+ action: !issue.readonly && !editingTitle && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25519
25776
  Button,
25520
25777
  {
25521
25778
  variant: "ghost",
@@ -25523,8 +25780,8 @@ var IssueDetails = ({
25523
25780
  children: "edit"
25524
25781
  }
25525
25782
  ),
25526
- children: editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
25527
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25783
+ children: editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
25784
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25528
25785
  Input,
25529
25786
  {
25530
25787
  value: title,
@@ -25536,11 +25793,11 @@ var IssueDetails = ({
25536
25793
  }
25537
25794
  }
25538
25795
  ),
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" })
25796
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(ActionRow, { children: [
25797
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: saveTitle, children: "save" }),
25798
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "ghost", onClick: cancelTitle, children: "cancel" })
25542
25799
  ] })
25543
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25800
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25544
25801
  "div",
25545
25802
  {
25546
25803
  style: {
@@ -25555,11 +25812,11 @@ var IssueDetails = ({
25555
25812
  )
25556
25813
  }
25557
25814
  ),
25558
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25815
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25559
25816
  Section,
25560
25817
  {
25561
25818
  title: "Description",
25562
- action: !issue.readonly && !editingDescription && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25819
+ action: !issue.readonly && !editingDescription && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25563
25820
  Button,
25564
25821
  {
25565
25822
  variant: "ghost",
@@ -25567,8 +25824,8 @@ var IssueDetails = ({
25567
25824
  children: "edit"
25568
25825
  }
25569
25826
  ),
25570
- children: editingDescription ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_jsx_runtime14.Fragment, { children: [
25571
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25827
+ children: editingDescription ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
25828
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25572
25829
  Textarea,
25573
25830
  {
25574
25831
  value: description,
@@ -25583,31 +25840,34 @@ var IssueDetails = ({
25583
25840
  }
25584
25841
  }
25585
25842
  ),
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" })
25843
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(ActionRow, { children: [
25844
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: saveDescription, children: "save" }),
25845
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "ghost", onClick: cancelDescription, children: "cancel" })
25589
25846
  ] })
25590
- ] }) : issue.description ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25847
+ ] }) : issue.description ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25591
25848
  "p",
25592
25849
  {
25593
25850
  style: {
25594
25851
  lineHeight: 1.55,
25595
25852
  whiteSpace: "pre-wrap",
25596
25853
  margin: "8px 0 0",
25854
+ padding: "12px 16px",
25855
+ background: "rgba(0, 0, 0, 0.22)",
25856
+ borderRadius: 8,
25597
25857
  color: GUI_THEME.primary
25598
25858
  },
25599
25859
  children: issue.description
25600
25860
  }
25601
- ) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Empty, { children: "No description" })
25861
+ ) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Empty, { children: "No description" })
25602
25862
  }
25603
25863
  ),
25604
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25864
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25605
25865
  Section,
25606
25866
  {
25607
25867
  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: "-" }),
25868
+ 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
25869
  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)(
25870
+ /* @__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
25871
  Button,
25612
25872
  {
25613
25873
  variant: "chip",
@@ -25623,7 +25883,7 @@ var IssueDetails = ({
25623
25883
  },
25624
25884
  tag.id
25625
25885
  )) }),
25626
- addingTag && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25886
+ addingTag && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25627
25887
  Button,
25628
25888
  {
25629
25889
  variant: "chip",
@@ -25638,8 +25898,8 @@ var IssueDetails = ({
25638
25898
  },
25639
25899
  tag.id
25640
25900
  )) }),
25641
- addingTag && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(AddRow, { children: [
25642
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25901
+ addingTag && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(AddRow, { children: [
25902
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25643
25903
  Input,
25644
25904
  {
25645
25905
  value: tagName,
@@ -25655,23 +25915,23 @@ var IssueDetails = ({
25655
25915
  }
25656
25916
  }
25657
25917
  ),
25658
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { onClick: addTag, children: "add" })
25918
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: addTag, children: "add" })
25659
25919
  ] })
25660
25920
  ]
25661
25921
  }
25662
25922
  ),
25663
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25923
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25664
25924
  Section,
25665
25925
  {
25666
25926
  title: "Assignees",
25667
- action: !issue.readonly && !addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25927
+ action: !issue.readonly && !addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25668
25928
  Button,
25669
25929
  {
25670
25930
  variant: "ghost",
25671
25931
  onClick: () => setAddingAssignee(true),
25672
25932
  children: "+"
25673
25933
  }
25674
- ) || addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25934
+ ) || addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25675
25935
  Button,
25676
25936
  {
25677
25937
  variant: "ghost",
@@ -25680,7 +25940,7 @@ var IssueDetails = ({
25680
25940
  }
25681
25941
  ),
25682
25942
  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)(
25943
+ /* @__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
25944
  Button,
25685
25945
  {
25686
25946
  variant: "chip",
@@ -25697,7 +25957,7 @@ var IssueDetails = ({
25697
25957
  },
25698
25958
  assignee.id
25699
25959
  )) }),
25700
- addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChipRow, { children: availableAssignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25960
+ addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChipRow, { children: availableAssignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25701
25961
  Button,
25702
25962
  {
25703
25963
  variant: "chip",
@@ -25712,8 +25972,8 @@ var IssueDetails = ({
25712
25972
  },
25713
25973
  assignee.id
25714
25974
  )) }),
25715
- addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(AddRow, { children: [
25716
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25975
+ addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(AddRow, { children: [
25976
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25717
25977
  Input,
25718
25978
  {
25719
25979
  value: assigneeName,
@@ -25729,21 +25989,32 @@ var IssueDetails = ({
25729
25989
  }
25730
25990
  }
25731
25991
  ),
25732
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Button, { onClick: addAssignee, children: "add" })
25992
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: addAssignee, children: "add" })
25733
25993
  ] })
25734
25994
  ]
25735
25995
  }
25736
25996
  ),
25737
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25997
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25998
+ IssueAttachments,
25999
+ {
26000
+ issueId: issue.id,
26001
+ readonly: Boolean(issue.readonly),
26002
+ attachments,
26003
+ uploadStatus: attachmentUploadStatus,
26004
+ onUploadFiles: onUploadAttachments,
26005
+ onDeleteAttachment
26006
+ }
26007
+ ),
26008
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25738
26009
  Section,
25739
26010
  {
25740
26011
  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" }),
26012
+ 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
26013
  children: ""
25743
26014
  }
25744
26015
  )
25745
26016
  ] }),
25746
- activeTab === "comments" && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
26017
+ activeTab === "comments" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25747
26018
  IssueComments,
25748
26019
  {
25749
26020
  whoAmI,
@@ -25754,15 +26025,15 @@ var IssueDetails = ({
25754
26025
  onDeleteComment
25755
26026
  }
25756
26027
  )
25757
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Empty, { children: "Select an issue" }) });
26028
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Empty, { children: "Select an issue" }) });
25758
26029
  };
25759
26030
 
25760
26031
  // source/gui/client/components/SwimlaneColumn.tsx
25761
- var import_react7 = __toESM(require_react(), 1);
26032
+ var import_react8 = __toESM(require_react(), 1);
25762
26033
 
25763
26034
  // 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)(
26035
+ var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
26036
+ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
25766
26037
  "svg",
25767
26038
  {
25768
26039
  width: size,
@@ -25770,7 +26041,7 @@ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime15.js
25770
26041
  viewBox: "0 0 16 16",
25771
26042
  fill: "none",
25772
26043
  "aria-hidden": "true",
25773
- children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26044
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
25774
26045
  "path",
25775
26046
  {
25776
26047
  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 +26056,7 @@ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime15.js
25785
26056
  );
25786
26057
 
25787
26058
  // source/gui/client/components/TicketCard.tsx
25788
- var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
26059
+ var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
25789
26060
  var TicketCard = ({
25790
26061
  ticket,
25791
26062
  index,
@@ -25798,7 +26069,7 @@ var TicketCard = ({
25798
26069
  onDropIssueAt
25799
26070
  }) => {
25800
26071
  const getVisualTargetIndex = (isAfterMiddle) => index + (isAfterMiddle ? 1 : 0);
25801
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
26072
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25802
26073
  "div",
25803
26074
  {
25804
26075
  draggable: !ticket.readonly,
@@ -25839,7 +26110,7 @@ var TicketCard = ({
25839
26110
  border: `1px solid ${isSelected ? GUI_THEME.accent : "transparent"}`
25840
26111
  },
25841
26112
  children: [
25842
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26113
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25843
26114
  "div",
25844
26115
  {
25845
26116
  style: {
@@ -25852,7 +26123,7 @@ var TicketCard = ({
25852
26123
  children: isSelected ? "\u276F" : index + 1
25853
26124
  }
25854
26125
  ),
25855
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
26126
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25856
26127
  "div",
25857
26128
  {
25858
26129
  style: {
@@ -25863,7 +26134,7 @@ var TicketCard = ({
25863
26134
  gap: 12
25864
26135
  },
25865
26136
  children: [
25866
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
26137
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25867
26138
  "div",
25868
26139
  {
25869
26140
  style: {
@@ -25874,7 +26145,7 @@ var TicketCard = ({
25874
26145
  flex: 1
25875
26146
  },
25876
26147
  children: [
25877
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26148
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25878
26149
  "div",
25879
26150
  {
25880
26151
  style: {
@@ -25884,10 +26155,10 @@ var TicketCard = ({
25884
26155
  fontWeight: 500,
25885
26156
  fontSize: 12
25886
26157
  },
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 }) })
26158
+ 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
26159
  }
25889
26160
  ),
25890
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26161
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25891
26162
  "div",
25892
26163
  {
25893
26164
  style: {
@@ -25900,7 +26171,7 @@ var TicketCard = ({
25900
26171
  children: ticket.title
25901
26172
  }
25902
26173
  ),
25903
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26174
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25904
26175
  "div",
25905
26176
  {
25906
26177
  style: {
@@ -25909,7 +26180,7 @@ var TicketCard = ({
25909
26180
  gap: 6,
25910
26181
  alignItems: "center"
25911
26182
  },
25912
- children: ticket.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26183
+ children: ticket.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25913
26184
  "span",
25914
26185
  {
25915
26186
  style: {
@@ -25929,7 +26200,7 @@ var TicketCard = ({
25929
26200
  ]
25930
26201
  }
25931
26202
  ),
25932
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26203
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25933
26204
  "div",
25934
26205
  {
25935
26206
  style: {
@@ -25939,7 +26210,7 @@ var TicketCard = ({
25939
26210
  flexShrink: 0,
25940
26211
  paddingTop: 2
25941
26212
  },
25942
- children: commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
26213
+ children: commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25943
26214
  "button",
25944
26215
  {
25945
26216
  type: "button",
@@ -25977,14 +26248,14 @@ var TicketCard = ({
25977
26248
  event.currentTarget.style.transform = "translateY(0)";
25978
26249
  },
25979
26250
  children: [
25980
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(IconComment, {}),
25981
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: commentCount })
26251
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(IconComment, {}),
26252
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: commentCount })
25982
26253
  ]
25983
26254
  }
25984
26255
  )
25985
26256
  }
25986
26257
  ),
25987
- ticket.assignees.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26258
+ ticket.assignees.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25988
26259
  "div",
25989
26260
  {
25990
26261
  style: {
@@ -25994,7 +26265,7 @@ var TicketCard = ({
25994
26265
  paddingTop: 2,
25995
26266
  marginTop: "-4px"
25996
26267
  },
25997
- children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(User, { user: assignee, index: idx, isFocus: isSelected }))
26268
+ children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(User, { user: assignee, index: idx, isFocus: isSelected }))
25998
26269
  }
25999
26270
  )
26000
26271
  ]
@@ -26006,7 +26277,7 @@ var TicketCard = ({
26006
26277
  };
26007
26278
 
26008
26279
  // source/gui/client/components/SwimlaneColumn.tsx
26009
- var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
26280
+ var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
26010
26281
  var SwimlaneColumn = ({
26011
26282
  swimlane,
26012
26283
  selected,
@@ -26022,7 +26293,7 @@ var SwimlaneColumn = ({
26022
26293
  onDragOverIssue,
26023
26294
  onDragLeave
26024
26295
  }) => {
26025
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
26296
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
26026
26297
  Panel,
26027
26298
  {
26028
26299
  as: "section",
@@ -26062,18 +26333,19 @@ var SwimlaneColumn = ({
26062
26333
  onDragLeave();
26063
26334
  },
26064
26335
  children: [
26065
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
26336
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
26066
26337
  "header",
26067
26338
  {
26068
26339
  style: {
26069
26340
  height: 48,
26341
+ flexShrink: 0,
26070
26342
  display: "flex",
26071
26343
  fontSize: 12,
26072
26344
  justifyContent: "space-between",
26073
26345
  alignItems: "center"
26074
26346
  },
26075
26347
  children: [
26076
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
26348
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
26077
26349
  "div",
26078
26350
  {
26079
26351
  style: {
@@ -26082,30 +26354,30 @@ var SwimlaneColumn = ({
26082
26354
  gap: 8
26083
26355
  },
26084
26356
  children: [
26085
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
26357
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26086
26358
  "span",
26087
26359
  {
26088
26360
  style: { color: selected ? GUI_THEME.accent : GUI_THEME.secondary },
26089
26361
  children: selected ? "\u276F" : " "
26090
26362
  }
26091
26363
  ),
26092
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
26364
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26093
26365
  "strong",
26094
26366
  {
26095
26367
  style: { color: selected ? GUI_THEME.accent : GUI_THEME.primary },
26096
26368
  children: swimlane.title
26097
26369
  }
26098
26370
  ),
26099
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { style: { color: GUI_THEME.dim }, children: [
26371
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { style: { color: GUI_THEME.dim }, children: [
26100
26372
  "(",
26101
26373
  swimlane.issues.length,
26102
26374
  ")"
26103
26375
  ] }),
26104
- swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: "\u{1F512}" })
26376
+ swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "\u{1F512}" })
26105
26377
  ]
26106
26378
  }
26107
26379
  ),
26108
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
26380
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26109
26381
  Button,
26110
26382
  {
26111
26383
  variant: "ghost",
@@ -26118,9 +26390,9 @@ var SwimlaneColumn = ({
26118
26390
  ]
26119
26391
  }
26120
26392
  ),
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)(
26393
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { overflow: "auto", paddingTop: 4, flex: 1, minHeight: 0 }, children: swimlane.issues.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
26394
+ dropIndex === 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(DropIndicator, {}),
26395
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26124
26396
  "div",
26125
26397
  {
26126
26398
  style: {
@@ -26132,10 +26404,10 @@ var SwimlaneColumn = ({
26132
26404
  children: "Drop issue here"
26133
26405
  }
26134
26406
  )
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)(
26407
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
26408
+ swimlane.issues.map((ticket, index) => /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_react8.default.Fragment, { children: [
26409
+ dropIndex === index && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(DropIndicator, {}),
26410
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26139
26411
  TicketCard,
26140
26412
  {
26141
26413
  ticket,
@@ -26153,7 +26425,7 @@ var SwimlaneColumn = ({
26153
26425
  }
26154
26426
  )
26155
26427
  ] }, ticket.id)),
26156
- dropIndex === swimlane.issues.length && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(DropIndicator, {})
26428
+ dropIndex === swimlane.issues.length && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(DropIndicator, {})
26157
26429
  ] }) })
26158
26430
  ]
26159
26431
  }
@@ -26291,9 +26563,73 @@ var updateIssueInGuiState = (state, issueId, updateIssue) => ({
26291
26563
  }))
26292
26564
  });
26293
26565
 
26566
+ // source/gui/client/lib/compress-image.ts
26567
+ var MAX_LONG_EDGE = 1600;
26568
+ var DEFAULT_MAX_KB = 500;
26569
+ var QUALITY_STEPS = [0.8, 0.7, 0.6, 0.5, 0.4];
26570
+ var toBlob = (canvas, type, quality) => new Promise((resolve) => canvas.toBlob(resolve, type, quality));
26571
+ var replaceExtension = (name, ext) => {
26572
+ const base = name.replace(/\.[a-z0-9]+$/i, "");
26573
+ return `${base || "image"}.${ext}`;
26574
+ };
26575
+ var compressImage = async (file, maxKb = DEFAULT_MAX_KB) => {
26576
+ const maxBytes = maxKb * 1024;
26577
+ if (file.type === "image/gif") {
26578
+ if (file.size <= maxBytes) {
26579
+ return { blob: file, name: file.name };
26580
+ }
26581
+ return {
26582
+ error: `GIFs are attached as-is and this one exceeds ${maxKb} KB`
26583
+ };
26584
+ }
26585
+ let bitmap;
26586
+ try {
26587
+ bitmap = await createImageBitmap(file);
26588
+ } catch {
26589
+ return { error: "Not a supported image" };
26590
+ }
26591
+ const scale = Math.min(
26592
+ 1,
26593
+ MAX_LONG_EDGE / Math.max(bitmap.width, bitmap.height)
26594
+ );
26595
+ const canvas = document.createElement("canvas");
26596
+ canvas.width = Math.max(1, Math.round(bitmap.width * scale));
26597
+ canvas.height = Math.max(1, Math.round(bitmap.height * scale));
26598
+ const context = canvas.getContext("2d");
26599
+ if (!context) {
26600
+ bitmap.close();
26601
+ return { error: "Unable to process image" };
26602
+ }
26603
+ context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
26604
+ bitmap.close();
26605
+ for (const quality of QUALITY_STEPS) {
26606
+ const blob = await toBlob(canvas, "image/webp", quality);
26607
+ if (blob && blob.size <= maxBytes) {
26608
+ return { blob, name: replaceExtension(file.name, "webp") };
26609
+ }
26610
+ }
26611
+ return {
26612
+ error: `Unable to compress image under ${maxKb} KB`
26613
+ };
26614
+ };
26615
+ var blobToBase64 = (blob) => new Promise((resolve, reject) => {
26616
+ const reader = new FileReader();
26617
+ reader.onload = () => {
26618
+ const dataUrl = String(reader.result ?? "");
26619
+ const base64 = dataUrl.split(",")[1];
26620
+ if (!base64) {
26621
+ reject(new Error("Unable to encode image"));
26622
+ return;
26623
+ }
26624
+ resolve(base64);
26625
+ };
26626
+ reader.onerror = () => reject(new Error("Unable to read image"));
26627
+ reader.readAsDataURL(blob);
26628
+ });
26629
+
26294
26630
  // source/gui/client/App.tsx
26295
- var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
26296
- var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26631
+ var import_jsx_runtime19 = __toESM(require_jsx_runtime(), 1);
26632
+ var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26297
26633
  "div",
26298
26634
  {
26299
26635
  style: {
@@ -26308,34 +26644,51 @@ var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26308
26644
  var App = () => {
26309
26645
  const { boardId, issueId } = useParams();
26310
26646
  const [searchParams, setSearchParams] = useSearchParams();
26311
- const [connected, setConnected] = (0, import_react8.useState)(false);
26312
- const [syncStatus, setSyncStatus] = (0, import_react8.useState)({
26647
+ const [connected, setConnected] = (0, import_react9.useState)(false);
26648
+ const [syncStatus, setSyncStatus] = (0, import_react9.useState)({
26313
26649
  status: "synced",
26314
26650
  msg: "idle"
26315
26651
  });
26316
- const [state, setState] = (0, import_react8.useState)(null);
26317
- const [dragOverSwimlaneId, setDragOverSwimlaneId] = (0, import_react8.useState)(
26652
+ const [state, setState] = (0, import_react9.useState)(null);
26653
+ const [dragOverSwimlaneId, setDragOverSwimlaneId] = (0, import_react9.useState)(
26318
26654
  null
26319
26655
  );
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);
26656
+ const [dropTarget, setDropTarget] = (0, import_react9.useState)(null);
26657
+ const [boardMenuOpen, setBoardMenuOpen] = (0, import_react9.useState)(false);
26658
+ const [createIssueModal, setCreateIssueModal] = (0, import_react9.useState)(null);
26659
+ const boardMenuRef = (0, import_react9.useRef)(null);
26660
+ const socketRef = (0, import_react9.useRef)(null);
26325
26661
  const selectedTab = searchParams.get("tab") === "comments" ? "comments" : "overview";
26326
26662
  const navigate = useNavigate();
26327
26663
  const selectedBoard = (state && boardId ? findBoard(state, boardId) : null) ?? state?.boards[0] ?? null;
26328
26664
  const boardSlug = selectedBoard?.ref ?? boardId;
26329
26665
  const selectedIssue = state && issueId ? findIssue(state, issueId) : null;
26666
+ (0, import_react9.useEffect)(() => {
26667
+ const onPaste = (event) => {
26668
+ if (!selectedIssue || selectedIssue.readonly) return;
26669
+ const target = event.target;
26670
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) {
26671
+ return;
26672
+ }
26673
+ const files = Array.from(event.clipboardData?.items ?? []).filter((item) => item.kind === "file" && item.type.startsWith("image/")).map((item) => item.getAsFile()).filter((file) => Boolean(file));
26674
+ if (files.length === 0) return;
26675
+ event.preventDefault();
26676
+ void uploadIssueAttachments(selectedIssue.id, files);
26677
+ };
26678
+ window.addEventListener("paste", onPaste);
26679
+ return () => window.removeEventListener("paste", onPaste);
26680
+ });
26330
26681
  const closeIssueDetails = () => {
26331
26682
  if (!boardSlug) return;
26332
26683
  void navigate(`/board/${boardSlug}`);
26333
26684
  };
26334
26685
  const commentsByIssueId = state?.commentsByIssueId ?? {};
26686
+ const attachmentsByIssueId = state?.attachmentsByIssueId ?? {};
26687
+ const [attachmentUploadStatus, setAttachmentUploadStatus] = (0, import_react9.useState)({ state: "idle" });
26335
26688
  const requestState = () => {
26336
26689
  socketRef.current?.send(JSON.stringify({ type: "state:get" }));
26337
26690
  };
26338
- (0, import_react8.useEffect)(() => {
26691
+ (0, import_react9.useEffect)(() => {
26339
26692
  const socket = new WebSocket(
26340
26693
  `ws://${window.location.host}/ws${boardId ? `?boardId=${boardId}` : ""}`
26341
26694
  );
@@ -26379,12 +26732,12 @@ var App = () => {
26379
26732
  socket.close();
26380
26733
  };
26381
26734
  }, [boardId, navigate]);
26382
- (0, import_react8.useEffect)(() => {
26735
+ (0, import_react9.useEffect)(() => {
26383
26736
  if (!boardId && state?.boards[0]) {
26384
26737
  void navigate(`/board/${state.boards[0].ref}`, { replace: true });
26385
26738
  }
26386
26739
  }, [boardId, state, navigate]);
26387
- (0, import_react8.useEffect)(() => {
26740
+ (0, import_react9.useEffect)(() => {
26388
26741
  const close = (event) => {
26389
26742
  if (boardMenuRef.current && !boardMenuRef.current.contains(event.target)) {
26390
26743
  setBoardMenuOpen(false);
@@ -26569,6 +26922,68 @@ var App = () => {
26569
26922
  });
26570
26923
  send("issue:comment:add", { issueId: issueId2, body });
26571
26924
  };
26925
+ const uploadIssueAttachments = async (issueId2, files) => {
26926
+ for (const file of files) {
26927
+ setAttachmentUploadStatus({ state: "uploading", name: file.name });
26928
+ const compressed = await compressImage(file, state?.attachmentMaxKb);
26929
+ if ("error" in compressed) {
26930
+ setAttachmentUploadStatus({ state: "error", message: compressed.error });
26931
+ return;
26932
+ }
26933
+ try {
26934
+ const dataBase64 = await blobToBase64(compressed.blob);
26935
+ const response = await fetch("/api/attachments", {
26936
+ method: "POST",
26937
+ headers: { "content-type": "application/json" },
26938
+ body: JSON.stringify({
26939
+ issueId: issueId2,
26940
+ name: compressed.name,
26941
+ dataBase64
26942
+ })
26943
+ });
26944
+ const payload = await response.json();
26945
+ if (!response.ok) {
26946
+ setAttachmentUploadStatus({
26947
+ state: "error",
26948
+ message: payload?.message ?? "Upload failed"
26949
+ });
26950
+ return;
26951
+ }
26952
+ const nextState = getResultValue(payload);
26953
+ if (nextState) setState(nextState);
26954
+ } catch (error) {
26955
+ setAttachmentUploadStatus({
26956
+ state: "error",
26957
+ message: error instanceof Error ? error.message : "Upload failed"
26958
+ });
26959
+ return;
26960
+ }
26961
+ }
26962
+ setAttachmentUploadStatus({ state: "idle" });
26963
+ };
26964
+ const deleteIssueAttachment = async (_issueId, attachmentId) => {
26965
+ try {
26966
+ const response = await fetch(
26967
+ `/api/attachments/${encodeURIComponent(attachmentId)}`,
26968
+ { method: "DELETE" }
26969
+ );
26970
+ const payload = await response.json();
26971
+ if (!response.ok) {
26972
+ setAttachmentUploadStatus({
26973
+ state: "error",
26974
+ message: payload?.message ?? "Unable to delete attachment"
26975
+ });
26976
+ return;
26977
+ }
26978
+ const nextState = getResultValue(payload);
26979
+ if (nextState) setState(nextState);
26980
+ } catch (error) {
26981
+ setAttachmentUploadStatus({
26982
+ state: "error",
26983
+ message: error instanceof Error ? error.message : "Unable to delete attachment"
26984
+ });
26985
+ }
26986
+ };
26572
26987
  const deleteIssueComment = (_issueId, commentId) => {
26573
26988
  setState((prev) => {
26574
26989
  if (!prev) return prev;
@@ -26586,7 +27001,7 @@ var App = () => {
26586
27001
  });
26587
27002
  send("issue:comment:delete", { commentId });
26588
27003
  };
26589
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
27004
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
26590
27005
  "div",
26591
27006
  {
26592
27007
  style: {
@@ -26598,8 +27013,8 @@ var App = () => {
26598
27013
  flexDirection: "column"
26599
27014
  },
26600
27015
  children: [
26601
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Header, { state, connected, syncStatus }),
26602
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
27016
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Header, { state, connected, syncStatus }),
27017
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
26603
27018
  "div",
26604
27019
  {
26605
27020
  style: {
@@ -26608,8 +27023,8 @@ var App = () => {
26608
27023
  overflow: "hidden"
26609
27024
  },
26610
27025
  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)(
27026
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("main", { style: { padding: "0 30px 30px 30px", overflow: "auto", flex: 1 }, children: [
27027
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { style: { padding: "20px 10px" }, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26613
27028
  Dropdown,
26614
27029
  {
26615
27030
  label: "Board:",
@@ -26625,7 +27040,7 @@ var App = () => {
26625
27040
  onSelect: selectBoard
26626
27041
  }
26627
27042
  ) }),
26628
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { display: "flex", gap: 8 }, children: selectedBoard?.swimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
27043
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { style: { display: "flex", gap: 8 }, children: selectedBoard?.swimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26629
27044
  SwimlaneColumn,
26630
27045
  {
26631
27046
  swimlane,
@@ -26645,7 +27060,7 @@ var App = () => {
26645
27060
  swimlane.id
26646
27061
  )) })
26647
27062
  ] }),
26648
- selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
27063
+ selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26649
27064
  IssueDetails,
26650
27065
  {
26651
27066
  whoAmI: state.user,
@@ -26662,6 +27077,10 @@ var App = () => {
26662
27077
  onRemoveAssignee: removeIssueAssignee,
26663
27078
  onAddComment: addIssueComment,
26664
27079
  onDeleteComment: deleteIssueComment,
27080
+ attachments: attachmentsByIssueId[selectedIssue.id] ?? [],
27081
+ attachmentUploadStatus,
27082
+ onUploadAttachments: uploadIssueAttachments,
27083
+ onDeleteAttachment: deleteIssueAttachment,
26665
27084
  onReopenIssue: reopenIssue,
26666
27085
  onCloseIssue: closeIssue,
26667
27086
  knownTags: state.tags ?? [],
@@ -26671,7 +27090,7 @@ var App = () => {
26671
27090
  ]
26672
27091
  }
26673
27092
  ),
26674
- createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
27093
+ createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26675
27094
  CreateIssueModal,
26676
27095
  {
26677
27096
  title: createIssueModal.title,
@@ -26686,14 +27105,14 @@ var App = () => {
26686
27105
  };
26687
27106
 
26688
27107
  // source/gui/client/main.tsx
26689
- var import_jsx_runtime19 = __toESM(require_jsx_runtime(), 1);
27108
+ var import_jsx_runtime20 = __toESM(require_jsx_runtime(), 1);
26690
27109
  (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 }) })
27110
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(BrowserRouter, { children: /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(Routes, { children: [
27111
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Route, { path: "/", element: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(App, {}) }),
27112
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Route, { path: "/board/:boardId", element: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(App, {}) }),
27113
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Route, { path: "/board/:boardId/issue/:issueId", element: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(App, {}) }),
27114
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Route, { path: "/board/:boardId/:issueId", element: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(App, {}) }),
27115
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Route, { path: "*", element: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(Navigate, { to: "/", replace: true }) })
26697
27116
  ] }) })
26698
27117
  );
26699
27118
  /*! Bundled license information: