epiq 1.2.2 → 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_react7 = __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.2";
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_react5 = __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);
@@ -25062,9 +25068,60 @@ var Aside = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
25062
25068
  }
25063
25069
  );
25064
25070
 
25065
- // source/gui/client/components/FormHeader.tsx
25071
+ // source/gui/client/components/CopyRef.tsx
25072
+ var import_react4 = __toESM(require_react(), 1);
25066
25073
  var import_jsx_runtime8 = __toESM(require_jsx_runtime(), 1);
25067
- var FormHeader = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
25074
+ var copyToClipboard = async (value) => {
25075
+ try {
25076
+ await navigator.clipboard.writeText(value);
25077
+ return true;
25078
+ } catch {
25079
+ return false;
25080
+ }
25081
+ };
25082
+ var CopyRef = ({ refValue }) => {
25083
+ const [copied, setCopied] = (0, import_react4.useState)(false);
25084
+ const resetTimer = (0, import_react4.useRef)(
25085
+ void 0
25086
+ );
25087
+ (0, import_react4.useEffect)(() => () => clearTimeout(resetTimer.current), []);
25088
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
25089
+ "button",
25090
+ {
25091
+ type: "button",
25092
+ title: `Copy ${refValue}`,
25093
+ onClick: async (event) => {
25094
+ event.stopPropagation();
25095
+ if (await copyToClipboard(refValue)) {
25096
+ setCopied(true);
25097
+ clearTimeout(resetTimer.current);
25098
+ resetTimer.current = setTimeout(() => setCopied(false), 1200);
25099
+ }
25100
+ },
25101
+ style: {
25102
+ fontFamily: "monospace",
25103
+ fontSize: "inherit",
25104
+ color: copied ? GUI_THEME.green : GUI_THEME.dim,
25105
+ background: "transparent",
25106
+ border: "none",
25107
+ padding: 0,
25108
+ cursor: "pointer",
25109
+ transition: "color 120ms ease"
25110
+ },
25111
+ onMouseEnter: (event) => {
25112
+ if (!copied) event.currentTarget.style.color = GUI_THEME.accent;
25113
+ },
25114
+ onMouseLeave: (event) => {
25115
+ event.currentTarget.style.color = copied ? GUI_THEME.green : GUI_THEME.dim;
25116
+ },
25117
+ children: copied ? "copied!" : refValue
25118
+ }
25119
+ );
25120
+ };
25121
+
25122
+ // source/gui/client/components/FormHeader.tsx
25123
+ var import_jsx_runtime9 = __toESM(require_jsx_runtime(), 1);
25124
+ var FormHeader = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
25068
25125
  "div",
25069
25126
  {
25070
25127
  style: {
@@ -25079,8 +25136,8 @@ var FormHeader = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
25079
25136
  );
25080
25137
 
25081
25138
  // source/gui/client/components/FormPrimitives.tsx
25082
- var import_jsx_runtime9 = __toESM(require_jsx_runtime(), 1);
25083
- var Input = (props) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
25139
+ var import_jsx_runtime10 = __toESM(require_jsx_runtime(), 1);
25140
+ var Input = (props) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25084
25141
  "input",
25085
25142
  {
25086
25143
  ...props,
@@ -25098,7 +25155,7 @@ var Input = (props) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
25098
25155
  }
25099
25156
  }
25100
25157
  );
25101
- var Textarea = (props) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
25158
+ var Textarea = (props) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25102
25159
  "textarea",
25103
25160
  {
25104
25161
  maxLength: props.maxLength ?? 1500,
@@ -25120,19 +25177,309 @@ var Textarea = (props) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
25120
25177
  }
25121
25178
  }
25122
25179
  );
25123
- var ActionRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
25180
+ var ActionRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25124
25181
  "div",
25125
25182
  {
25126
25183
  style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 8 },
25127
25184
  children
25128
25185
  }
25129
25186
  );
25130
- var AddRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { display: "flex", gap: 8, marginTop: 10 }, children });
25131
- var ChipRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10 }, children });
25132
- var Empty = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { display: "inline-block", marginTop: 8, color: GUI_THEME.dim }, children });
25187
+ var AddRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { display: "flex", gap: 8, marginTop: 10 }, children });
25188
+ var ChipRow = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 10 }, children });
25189
+ var Empty = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { style: { display: "inline-block", marginTop: 8, color: GUI_THEME.dim }, children });
25190
+
25191
+ // source/gui/client/components/IssueAttachments.tsx
25192
+ var import_react5 = __toESM(require_react(), 1);
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
+ };
25133
25480
 
25134
25481
  // source/gui/client/components/IssueComments.tsx
25135
- var import_react4 = __toESM(require_react(), 1);
25482
+ var import_react6 = __toESM(require_react(), 1);
25136
25483
 
25137
25484
  // source/gui/client/lib/gui-format.helper.ts
25138
25485
  var timeAgo = (timestampMs) => {
@@ -25154,7 +25501,7 @@ var timeAgo = (timestampMs) => {
25154
25501
  };
25155
25502
 
25156
25503
  // source/gui/client/components/IssueComments.tsx
25157
- var import_jsx_runtime10 = __toESM(require_jsx_runtime(), 1);
25504
+ var import_jsx_runtime13 = __toESM(require_jsx_runtime(), 1);
25158
25505
  var IssueComments = ({
25159
25506
  whoAmI,
25160
25507
  issueId,
@@ -25163,15 +25510,15 @@ var IssueComments = ({
25163
25510
  onAddComment,
25164
25511
  onDeleteComment
25165
25512
  }) => {
25166
- const [body, setBody] = (0, import_react4.useState)("");
25513
+ const [body, setBody] = (0, import_react6.useState)("");
25167
25514
  const addComment = () => {
25168
25515
  const nextBody = body.trim();
25169
25516
  if (!nextBody) return;
25170
25517
  onAddComment?.(issueId, nextBody);
25171
25518
  setBody("");
25172
25519
  };
25173
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { children: [
25174
- comments.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(Empty, { children: "No comments" }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime10.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)(
25175
25522
  "div",
25176
25523
  {
25177
25524
  style: {
@@ -25181,7 +25528,7 @@ var IssueComments = ({
25181
25528
  background: GUI_THEME.tertiary
25182
25529
  },
25183
25530
  children: [
25184
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
25531
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25185
25532
  "div",
25186
25533
  {
25187
25534
  style: {
@@ -25191,15 +25538,15 @@ var IssueComments = ({
25191
25538
  marginBottom: 8
25192
25539
  },
25193
25540
  children: [
25194
- /* @__PURE__ */ (0, import_jsx_runtime10.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: [
25195
25542
  comment.author.name ?? "unknown",
25196
- comment.createdAt && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { style: { color: GUI_THEME.dim2 }, children: [
25543
+ comment.createdAt && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { style: { color: GUI_THEME.dim2 }, children: [
25197
25544
  " ",
25198
25545
  "\xB7 ",
25199
25546
  timeAgo(comment.createdAt)
25200
25547
  ] })
25201
25548
  ] }),
25202
- comment.author.id === whoAmI.id && onDeleteComment && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25549
+ comment.author.id === whoAmI.id && onDeleteComment && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25203
25550
  Button,
25204
25551
  {
25205
25552
  variant: "ghost",
@@ -25214,7 +25561,7 @@ var IssueComments = ({
25214
25561
  ]
25215
25562
  }
25216
25563
  ),
25217
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25564
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25218
25565
  "div",
25219
25566
  {
25220
25567
  style: {
@@ -25229,8 +25576,8 @@ var IssueComments = ({
25229
25576
  },
25230
25577
  comment.id
25231
25578
  )) }),
25232
- !readonly && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { marginTop: 18 }, children: [
25233
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
25579
+ !readonly && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: { marginTop: 18 }, children: [
25580
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25234
25581
  Textarea,
25235
25582
  {
25236
25583
  maxLength: 120,
@@ -25245,64 +25592,18 @@ var IssueComments = ({
25245
25592
  style: { minHeight: 45 }
25246
25593
  }
25247
25594
  ),
25248
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ActionRow, { children: /* @__PURE__ */ (0, import_jsx_runtime10.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" }) })
25249
25596
  ] })
25250
25597
  ] });
25251
25598
  };
25252
25599
 
25253
- // source/gui/client/components/Section.tsx
25254
- var import_jsx_runtime11 = __toESM(require_jsx_runtime(), 1);
25255
- var Section = ({
25256
- title,
25257
- action,
25258
- children,
25259
- first = false
25260
- }) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
25261
- "section",
25262
- {
25263
- style: {
25264
- padding: first ? "0px 0 14px 0" : "14px 0",
25265
- borderTop: first ? "none" : `1px solid ${GUI_THEME.line}`
25266
- },
25267
- children: [
25268
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
25269
- "div",
25270
- {
25271
- style: {
25272
- display: "flex",
25273
- justifyContent: "space-between",
25274
- alignItems: "center",
25275
- gap: 12
25276
- },
25277
- children: [
25278
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
25279
- "span",
25280
- {
25281
- style: {
25282
- color: GUI_THEME.secondary,
25283
- fontSize: 10,
25284
- textTransform: "uppercase",
25285
- letterSpacing: "0.08em"
25286
- },
25287
- children: title
25288
- }
25289
- ),
25290
- action
25291
- ]
25292
- }
25293
- ),
25294
- children
25295
- ]
25296
- }
25297
- );
25298
-
25299
25600
  // source/gui/client/components/Tabs.tsx
25300
- var import_jsx_runtime12 = __toESM(require_jsx_runtime(), 1);
25601
+ var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
25301
25602
  var Tabs = ({
25302
25603
  activeTab,
25303
25604
  tabs,
25304
25605
  onChange
25305
- }) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
25606
+ }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
25306
25607
  "div",
25307
25608
  {
25308
25609
  style: {
@@ -25313,7 +25614,7 @@ var Tabs = ({
25313
25614
  },
25314
25615
  children: tabs.map((tab) => {
25315
25616
  const active = tab.id === activeTab;
25316
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25617
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25317
25618
  "button",
25318
25619
  {
25319
25620
  type: "button",
@@ -25331,7 +25632,7 @@ var Tabs = ({
25331
25632
  },
25332
25633
  children: [
25333
25634
  tab.label,
25334
- typeof tab.count === "number" && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
25635
+ typeof tab.count === "number" && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
25335
25636
  "span",
25336
25637
  {
25337
25638
  style: { color: tab.count ? GUI_THEME.secondary : GUI_THEME.dim },
@@ -25352,7 +25653,7 @@ var Tabs = ({
25352
25653
  );
25353
25654
 
25354
25655
  // source/gui/client/components/IssueDetails.tsx
25355
- var import_jsx_runtime13 = __toESM(require_jsx_runtime(), 1);
25656
+ var import_jsx_runtime15 = __toESM(require_jsx_runtime(), 1);
25356
25657
  var IssueDetails = ({
25357
25658
  whoAmI,
25358
25659
  comments,
@@ -25370,18 +25671,22 @@ var IssueDetails = ({
25370
25671
  onReopenIssue,
25371
25672
  onAddComment,
25372
25673
  onDeleteComment,
25674
+ attachments,
25675
+ attachmentUploadStatus,
25676
+ onUploadAttachments,
25677
+ onDeleteAttachment,
25373
25678
  knownTags: tags,
25374
25679
  knownAssignees: assignees
25375
25680
  }) => {
25376
- const [title, setTitle] = (0, import_react5.useState)("");
25377
- const [description, setDescription] = (0, import_react5.useState)("");
25378
- const [tagName, setTagName] = (0, import_react5.useState)("");
25379
- const [assigneeName, setAssigneeName] = (0, import_react5.useState)("");
25380
- const [editingTitle, setEditingTitle] = (0, import_react5.useState)(false);
25381
- const [editingDescription, setEditingDescription] = (0, import_react5.useState)(false);
25382
- const [addingTag, setAddingTag] = (0, import_react5.useState)(false);
25383
- const [addingAssignee, setAddingAssignee] = (0, import_react5.useState)(false);
25384
- (0, import_react5.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)(() => {
25385
25690
  setTitle(issue?.title ?? "");
25386
25691
  setDescription(issue?.description ?? "");
25387
25692
  setTagName("");
@@ -25441,9 +25746,9 @@ var IssueDetails = ({
25441
25746
  const availableAssignees = assignees.filter(
25442
25747
  (assignee) => !issue?.assignees.some((issueAssignee) => issueAssignee.id === assignee.id)
25443
25748
  );
25444
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Aside, { children: issue ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
25445
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(FormHeader, { children: [
25446
- /* @__PURE__ */ (0, import_jsx_runtime13.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)(
25447
25752
  "span",
25448
25753
  {
25449
25754
  style: {
@@ -25452,19 +25757,19 @@ var IssueDetails = ({
25452
25757
  textTransform: "uppercase",
25453
25758
  letterSpacing: "0.08em"
25454
25759
  },
25455
- children: issue.ref && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { style: { fontFamily: "monospace", color: GUI_THEME.dim }, children: "#" + issue.ref })
25760
+ children: issue.ref && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CopyRef, { refValue: issue.ref })
25456
25761
  }
25457
25762
  ),
25458
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { variant: "ghost", onClick: onClose, children: "\xD7" })
25763
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { variant: "ghost", onClick: onClose, children: "\xD7" })
25459
25764
  ] }),
25460
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Tabs, { tabs, activeTab, onChange: onChangeTab }),
25461
- activeTab === "overview" && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
25462
- /* @__PURE__ */ (0, import_jsx_runtime13.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)(
25463
25768
  Section,
25464
25769
  {
25465
25770
  first: true,
25466
25771
  title: "Title",
25467
- action: !issue.readonly && !editingTitle && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25772
+ action: !issue.readonly && !editingTitle && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25468
25773
  Button,
25469
25774
  {
25470
25775
  variant: "ghost",
@@ -25472,8 +25777,8 @@ var IssueDetails = ({
25472
25777
  children: "edit"
25473
25778
  }
25474
25779
  ),
25475
- children: editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
25476
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25780
+ children: editingTitle ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
25781
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25477
25782
  Input,
25478
25783
  {
25479
25784
  value: title,
@@ -25485,11 +25790,11 @@ var IssueDetails = ({
25485
25790
  }
25486
25791
  }
25487
25792
  ),
25488
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(ActionRow, { children: [
25489
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { onClick: saveTitle, children: "save" }),
25490
- /* @__PURE__ */ (0, import_jsx_runtime13.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" })
25491
25796
  ] })
25492
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25797
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25493
25798
  "div",
25494
25799
  {
25495
25800
  style: {
@@ -25504,11 +25809,11 @@ var IssueDetails = ({
25504
25809
  )
25505
25810
  }
25506
25811
  ),
25507
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25812
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25508
25813
  Section,
25509
25814
  {
25510
25815
  title: "Description",
25511
- action: !issue.readonly && !editingDescription && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25816
+ action: !issue.readonly && !editingDescription && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25512
25817
  Button,
25513
25818
  {
25514
25819
  variant: "ghost",
@@ -25516,8 +25821,8 @@ var IssueDetails = ({
25516
25821
  children: "edit"
25517
25822
  }
25518
25823
  ),
25519
- children: editingDescription ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
25520
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25824
+ children: editingDescription ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
25825
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25521
25826
  Textarea,
25522
25827
  {
25523
25828
  value: description,
@@ -25532,31 +25837,34 @@ var IssueDetails = ({
25532
25837
  }
25533
25838
  }
25534
25839
  ),
25535
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(ActionRow, { children: [
25536
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { onClick: saveDescription, children: "save" }),
25537
- /* @__PURE__ */ (0, import_jsx_runtime13.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" })
25538
25843
  ] })
25539
- ] }) : issue.description ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25844
+ ] }) : issue.description ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25540
25845
  "p",
25541
25846
  {
25542
25847
  style: {
25543
25848
  lineHeight: 1.55,
25544
25849
  whiteSpace: "pre-wrap",
25545
25850
  margin: "8px 0 0",
25851
+ padding: "12px 16px",
25852
+ background: "rgba(0, 0, 0, 0.22)",
25853
+ borderRadius: 8,
25546
25854
  color: GUI_THEME.primary
25547
25855
  },
25548
25856
  children: issue.description
25549
25857
  }
25550
- ) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Empty, { children: "No description" })
25858
+ ) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Empty, { children: "No description" })
25551
25859
  }
25552
25860
  ),
25553
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25861
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25554
25862
  Section,
25555
25863
  {
25556
25864
  title: "Tags",
25557
- action: !issue.readonly && !addingTag && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { variant: "ghost", onClick: () => setAddingTag(true), children: "+" }) || addingTag && /* @__PURE__ */ (0, import_jsx_runtime13.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: "-" }),
25558
25866
  children: [
25559
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChipRow, { children: issue.tags.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Empty, { children: "No tags" }) : issue.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime13.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)(
25560
25868
  Button,
25561
25869
  {
25562
25870
  variant: "chip",
@@ -25572,7 +25880,7 @@ var IssueDetails = ({
25572
25880
  },
25573
25881
  tag.id
25574
25882
  )) }),
25575
- addingTag && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25883
+ addingTag && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChipRow, { children: availableTags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25576
25884
  Button,
25577
25885
  {
25578
25886
  variant: "chip",
@@ -25587,8 +25895,8 @@ var IssueDetails = ({
25587
25895
  },
25588
25896
  tag.id
25589
25897
  )) }),
25590
- addingTag && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(AddRow, { children: [
25591
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25898
+ addingTag && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(AddRow, { children: [
25899
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25592
25900
  Input,
25593
25901
  {
25594
25902
  value: tagName,
@@ -25604,23 +25912,23 @@ var IssueDetails = ({
25604
25912
  }
25605
25913
  }
25606
25914
  ),
25607
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { onClick: addTag, children: "add" })
25915
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: addTag, children: "add" })
25608
25916
  ] })
25609
25917
  ]
25610
25918
  }
25611
25919
  ),
25612
- /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25920
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25613
25921
  Section,
25614
25922
  {
25615
25923
  title: "Assignees",
25616
- action: !issue.readonly && !addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25924
+ action: !issue.readonly && !addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25617
25925
  Button,
25618
25926
  {
25619
25927
  variant: "ghost",
25620
25928
  onClick: () => setAddingAssignee(true),
25621
25929
  children: "+"
25622
25930
  }
25623
- ) || addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25931
+ ) || addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25624
25932
  Button,
25625
25933
  {
25626
25934
  variant: "ghost",
@@ -25629,7 +25937,7 @@ var IssueDetails = ({
25629
25937
  }
25630
25938
  ),
25631
25939
  children: [
25632
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChipRow, { children: issue.assignees.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Empty, { children: "No assignees" }) : issue.assignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime13.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)(
25633
25941
  Button,
25634
25942
  {
25635
25943
  variant: "chip",
@@ -25646,7 +25954,7 @@ var IssueDetails = ({
25646
25954
  },
25647
25955
  assignee.id
25648
25956
  )) }),
25649
- addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChipRow, { children: availableAssignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
25957
+ addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChipRow, { children: availableAssignees.map((assignee) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25650
25958
  Button,
25651
25959
  {
25652
25960
  variant: "chip",
@@ -25661,8 +25969,8 @@ var IssueDetails = ({
25661
25969
  },
25662
25970
  assignee.id
25663
25971
  )) }),
25664
- addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(AddRow, { children: [
25665
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
25972
+ addingAssignee && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(AddRow, { children: [
25973
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25666
25974
  Input,
25667
25975
  {
25668
25976
  value: assigneeName,
@@ -25678,21 +25986,32 @@ var IssueDetails = ({
25678
25986
  }
25679
25987
  }
25680
25988
  ),
25681
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { onClick: addAssignee, children: "add" })
25989
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Button, { onClick: addAssignee, children: "add" })
25682
25990
  ] })
25683
25991
  ]
25684
25992
  }
25685
25993
  ),
25686
- /* @__PURE__ */ (0, import_jsx_runtime13.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)(
25687
26006
  Section,
25688
26007
  {
25689
26008
  title: "Actions",
25690
- action: issue.isClosed ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Button, { onClick: () => onReopenIssue(issue.id), children: "reopen issue" }) : /* @__PURE__ */ (0, import_jsx_runtime13.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" }),
25691
26010
  children: ""
25692
26011
  }
25693
26012
  )
25694
26013
  ] }),
25695
- activeTab === "comments" && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
26014
+ activeTab === "comments" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
25696
26015
  IssueComments,
25697
26016
  {
25698
26017
  whoAmI,
@@ -25703,15 +26022,15 @@ var IssueDetails = ({
25703
26022
  onDeleteComment
25704
26023
  }
25705
26024
  )
25706
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Empty, { children: "Select an issue" }) });
26025
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Empty, { children: "Select an issue" }) });
25707
26026
  };
25708
26027
 
25709
26028
  // source/gui/client/components/SwimlaneColumn.tsx
25710
- var import_react6 = __toESM(require_react(), 1);
26029
+ var import_react8 = __toESM(require_react(), 1);
25711
26030
 
25712
26031
  // source/gui/client/components/IconComment.tsx
25713
- var import_jsx_runtime14 = __toESM(require_jsx_runtime(), 1);
25714
- var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
26032
+ var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
26033
+ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
25715
26034
  "svg",
25716
26035
  {
25717
26036
  width: size,
@@ -25719,7 +26038,7 @@ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime14.js
25719
26038
  viewBox: "0 0 16 16",
25720
26039
  fill: "none",
25721
26040
  "aria-hidden": "true",
25722
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
26041
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
25723
26042
  "path",
25724
26043
  {
25725
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",
@@ -25734,7 +26053,7 @@ var IconComment = ({ size = 16 }) => /* @__PURE__ */ (0, import_jsx_runtime14.js
25734
26053
  );
25735
26054
 
25736
26055
  // source/gui/client/components/TicketCard.tsx
25737
- var import_jsx_runtime15 = __toESM(require_jsx_runtime(), 1);
26056
+ var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
25738
26057
  var TicketCard = ({
25739
26058
  ticket,
25740
26059
  index,
@@ -25747,7 +26066,7 @@ var TicketCard = ({
25747
26066
  onDropIssueAt
25748
26067
  }) => {
25749
26068
  const getVisualTargetIndex = (isAfterMiddle) => index + (isAfterMiddle ? 1 : 0);
25750
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
26069
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25751
26070
  "div",
25752
26071
  {
25753
26072
  draggable: !ticket.readonly,
@@ -25788,7 +26107,7 @@ var TicketCard = ({
25788
26107
  border: `1px solid ${isSelected ? GUI_THEME.accent : "transparent"}`
25789
26108
  },
25790
26109
  children: [
25791
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26110
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25792
26111
  "div",
25793
26112
  {
25794
26113
  style: {
@@ -25801,7 +26120,7 @@ var TicketCard = ({
25801
26120
  children: isSelected ? "\u276F" : index + 1
25802
26121
  }
25803
26122
  ),
25804
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
26123
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25805
26124
  "div",
25806
26125
  {
25807
26126
  style: {
@@ -25812,7 +26131,7 @@ var TicketCard = ({
25812
26131
  gap: 12
25813
26132
  },
25814
26133
  children: [
25815
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
26134
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25816
26135
  "div",
25817
26136
  {
25818
26137
  style: {
@@ -25823,7 +26142,7 @@ var TicketCard = ({
25823
26142
  flex: 1
25824
26143
  },
25825
26144
  children: [
25826
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26145
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25827
26146
  "div",
25828
26147
  {
25829
26148
  style: {
@@ -25833,23 +26152,10 @@ var TicketCard = ({
25833
26152
  fontWeight: 500,
25834
26153
  fontSize: 12
25835
26154
  },
25836
- children: ticket.ref && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
25837
- "span",
25838
- {
25839
- style: {
25840
- fontFamily: "monospace",
25841
- marginRight: 6,
25842
- color: GUI_THEME.dim
25843
- },
25844
- children: [
25845
- "#",
25846
- ticket.ref
25847
- ]
25848
- }
25849
- )
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 }) })
25850
26156
  }
25851
26157
  ),
25852
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26158
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25853
26159
  "div",
25854
26160
  {
25855
26161
  style: {
@@ -25862,7 +26168,7 @@ var TicketCard = ({
25862
26168
  children: ticket.title
25863
26169
  }
25864
26170
  ),
25865
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26171
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25866
26172
  "div",
25867
26173
  {
25868
26174
  style: {
@@ -25871,7 +26177,7 @@ var TicketCard = ({
25871
26177
  gap: 6,
25872
26178
  alignItems: "center"
25873
26179
  },
25874
- children: ticket.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26180
+ children: ticket.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25875
26181
  "span",
25876
26182
  {
25877
26183
  style: {
@@ -25891,7 +26197,7 @@ var TicketCard = ({
25891
26197
  ]
25892
26198
  }
25893
26199
  ),
25894
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26200
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25895
26201
  "div",
25896
26202
  {
25897
26203
  style: {
@@ -25901,7 +26207,7 @@ var TicketCard = ({
25901
26207
  flexShrink: 0,
25902
26208
  paddingTop: 2
25903
26209
  },
25904
- children: commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
26210
+ children: commentCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
25905
26211
  "button",
25906
26212
  {
25907
26213
  type: "button",
@@ -25939,14 +26245,14 @@ var TicketCard = ({
25939
26245
  event.currentTarget.style.transform = "translateY(0)";
25940
26246
  },
25941
26247
  children: [
25942
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(IconComment, {}),
25943
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { children: commentCount })
26248
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(IconComment, {}),
26249
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { children: commentCount })
25944
26250
  ]
25945
26251
  }
25946
26252
  )
25947
26253
  }
25948
26254
  ),
25949
- ticket.assignees.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
26255
+ ticket.assignees.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
25950
26256
  "div",
25951
26257
  {
25952
26258
  style: {
@@ -25956,7 +26262,7 @@ var TicketCard = ({
25956
26262
  paddingTop: 2,
25957
26263
  marginTop: "-4px"
25958
26264
  },
25959
- children: ticket.assignees.map((assignee, idx) => /* @__PURE__ */ (0, import_jsx_runtime15.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 }))
25960
26266
  }
25961
26267
  )
25962
26268
  ]
@@ -25968,7 +26274,7 @@ var TicketCard = ({
25968
26274
  };
25969
26275
 
25970
26276
  // source/gui/client/components/SwimlaneColumn.tsx
25971
- var import_jsx_runtime16 = __toESM(require_jsx_runtime(), 1);
26277
+ var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
25972
26278
  var SwimlaneColumn = ({
25973
26279
  swimlane,
25974
26280
  selected,
@@ -25984,7 +26290,7 @@ var SwimlaneColumn = ({
25984
26290
  onDragOverIssue,
25985
26291
  onDragLeave
25986
26292
  }) => {
25987
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
26293
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
25988
26294
  Panel,
25989
26295
  {
25990
26296
  as: "section",
@@ -26024,7 +26330,7 @@ var SwimlaneColumn = ({
26024
26330
  onDragLeave();
26025
26331
  },
26026
26332
  children: [
26027
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
26333
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
26028
26334
  "header",
26029
26335
  {
26030
26336
  style: {
@@ -26035,7 +26341,7 @@ var SwimlaneColumn = ({
26035
26341
  alignItems: "center"
26036
26342
  },
26037
26343
  children: [
26038
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
26344
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
26039
26345
  "div",
26040
26346
  {
26041
26347
  style: {
@@ -26044,30 +26350,30 @@ var SwimlaneColumn = ({
26044
26350
  gap: 8
26045
26351
  },
26046
26352
  children: [
26047
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26353
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26048
26354
  "span",
26049
26355
  {
26050
26356
  style: { color: selected ? GUI_THEME.accent : GUI_THEME.secondary },
26051
26357
  children: selected ? "\u276F" : " "
26052
26358
  }
26053
26359
  ),
26054
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26360
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26055
26361
  "strong",
26056
26362
  {
26057
26363
  style: { color: selected ? GUI_THEME.accent : GUI_THEME.primary },
26058
26364
  children: swimlane.title
26059
26365
  }
26060
26366
  ),
26061
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { style: { color: GUI_THEME.dim }, children: [
26367
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { style: { color: GUI_THEME.dim }, children: [
26062
26368
  "(",
26063
26369
  swimlane.issues.length,
26064
26370
  ")"
26065
26371
  ] }),
26066
- swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: "\u{1F512}" })
26372
+ swimlane.readonly && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { children: "\u{1F512}" })
26067
26373
  ]
26068
26374
  }
26069
26375
  ),
26070
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
26376
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
26071
26377
  Button,
26072
26378
  {
26073
26379
  variant: "ghost",
@@ -26080,9 +26386,9 @@ var SwimlaneColumn = ({
26080
26386
  ]
26081
26387
  }
26082
26388
  ),
26083
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: { overflow: "auto", paddingTop: 4, flex: 1 }, children: swimlane.issues.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
26084
- dropIndex === 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DropIndicator, {}),
26085
- /* @__PURE__ */ (0, import_jsx_runtime16.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)(
26086
26392
  "div",
26087
26393
  {
26088
26394
  style: {
@@ -26094,10 +26400,10 @@ var SwimlaneColumn = ({
26094
26400
  children: "Drop issue here"
26095
26401
  }
26096
26402
  )
26097
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
26098
- swimlane.issues.map((ticket, index) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_react6.default.Fragment, { children: [
26099
- dropIndex === index && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DropIndicator, {}),
26100
- /* @__PURE__ */ (0, import_jsx_runtime16.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)(
26101
26407
  TicketCard,
26102
26408
  {
26103
26409
  ticket,
@@ -26115,7 +26421,7 @@ var SwimlaneColumn = ({
26115
26421
  }
26116
26422
  )
26117
26423
  ] }, ticket.id)),
26118
- dropIndex === swimlane.issues.length && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(DropIndicator, {})
26424
+ dropIndex === swimlane.issues.length && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(DropIndicator, {})
26119
26425
  ] }) })
26120
26426
  ]
26121
26427
  }
@@ -26253,9 +26559,73 @@ var updateIssueInGuiState = (state, issueId, updateIssue) => ({
26253
26559
  }))
26254
26560
  });
26255
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
+
26256
26626
  // source/gui/client/App.tsx
26257
- var import_jsx_runtime17 = __toESM(require_jsx_runtime(), 1);
26258
- var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
26627
+ var import_jsx_runtime19 = __toESM(require_jsx_runtime(), 1);
26628
+ var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26259
26629
  "div",
26260
26630
  {
26261
26631
  style: {
@@ -26270,34 +26640,51 @@ var DropIndicator = () => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
26270
26640
  var App = () => {
26271
26641
  const { boardId, issueId } = useParams();
26272
26642
  const [searchParams, setSearchParams] = useSearchParams();
26273
- const [connected, setConnected] = (0, import_react7.useState)(false);
26274
- const [syncStatus, setSyncStatus] = (0, import_react7.useState)({
26643
+ const [connected, setConnected] = (0, import_react9.useState)(false);
26644
+ const [syncStatus, setSyncStatus] = (0, import_react9.useState)({
26275
26645
  status: "synced",
26276
26646
  msg: "idle"
26277
26647
  });
26278
- const [state, setState] = (0, import_react7.useState)(null);
26279
- const [dragOverSwimlaneId, setDragOverSwimlaneId] = (0, import_react7.useState)(
26648
+ const [state, setState] = (0, import_react9.useState)(null);
26649
+ const [dragOverSwimlaneId, setDragOverSwimlaneId] = (0, import_react9.useState)(
26280
26650
  null
26281
26651
  );
26282
- const [dropTarget, setDropTarget] = (0, import_react7.useState)(null);
26283
- const [boardMenuOpen, setBoardMenuOpen] = (0, import_react7.useState)(false);
26284
- const [createIssueModal, setCreateIssueModal] = (0, import_react7.useState)(null);
26285
- const boardMenuRef = (0, import_react7.useRef)(null);
26286
- const socketRef = (0, import_react7.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);
26287
26657
  const selectedTab = searchParams.get("tab") === "comments" ? "comments" : "overview";
26288
26658
  const navigate = useNavigate();
26289
26659
  const selectedBoard = (state && boardId ? findBoard(state, boardId) : null) ?? state?.boards[0] ?? null;
26290
26660
  const boardSlug = selectedBoard?.ref ?? boardId;
26291
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
+ });
26292
26677
  const closeIssueDetails = () => {
26293
26678
  if (!boardSlug) return;
26294
26679
  void navigate(`/board/${boardSlug}`);
26295
26680
  };
26296
26681
  const commentsByIssueId = state?.commentsByIssueId ?? {};
26682
+ const attachmentsByIssueId = state?.attachmentsByIssueId ?? {};
26683
+ const [attachmentUploadStatus, setAttachmentUploadStatus] = (0, import_react9.useState)({ state: "idle" });
26297
26684
  const requestState = () => {
26298
26685
  socketRef.current?.send(JSON.stringify({ type: "state:get" }));
26299
26686
  };
26300
- (0, import_react7.useEffect)(() => {
26687
+ (0, import_react9.useEffect)(() => {
26301
26688
  const socket = new WebSocket(
26302
26689
  `ws://${window.location.host}/ws${boardId ? `?boardId=${boardId}` : ""}`
26303
26690
  );
@@ -26341,12 +26728,12 @@ var App = () => {
26341
26728
  socket.close();
26342
26729
  };
26343
26730
  }, [boardId, navigate]);
26344
- (0, import_react7.useEffect)(() => {
26731
+ (0, import_react9.useEffect)(() => {
26345
26732
  if (!boardId && state?.boards[0]) {
26346
26733
  void navigate(`/board/${state.boards[0].ref}`, { replace: true });
26347
26734
  }
26348
26735
  }, [boardId, state, navigate]);
26349
- (0, import_react7.useEffect)(() => {
26736
+ (0, import_react9.useEffect)(() => {
26350
26737
  const close = (event) => {
26351
26738
  if (boardMenuRef.current && !boardMenuRef.current.contains(event.target)) {
26352
26739
  setBoardMenuOpen(false);
@@ -26531,6 +26918,68 @@ var App = () => {
26531
26918
  });
26532
26919
  send("issue:comment:add", { issueId: issueId2, body });
26533
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
+ };
26534
26983
  const deleteIssueComment = (_issueId, commentId) => {
26535
26984
  setState((prev) => {
26536
26985
  if (!prev) return prev;
@@ -26548,7 +26997,7 @@ var App = () => {
26548
26997
  });
26549
26998
  send("issue:comment:delete", { commentId });
26550
26999
  };
26551
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
27000
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
26552
27001
  "div",
26553
27002
  {
26554
27003
  style: {
@@ -26560,8 +27009,8 @@ var App = () => {
26560
27009
  flexDirection: "column"
26561
27010
  },
26562
27011
  children: [
26563
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Header, { state, connected, syncStatus }),
26564
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
27012
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(Header, { state, connected, syncStatus }),
27013
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
26565
27014
  "div",
26566
27015
  {
26567
27016
  style: {
@@ -26570,8 +27019,8 @@ var App = () => {
26570
27019
  overflow: "hidden"
26571
27020
  },
26572
27021
  children: [
26573
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("main", { style: { padding: "0 30px 30px 30px", overflow: "auto", flex: 1 }, children: [
26574
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: { padding: "20px 10px" }, children: /* @__PURE__ */ (0, import_jsx_runtime17.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)(
26575
27024
  Dropdown,
26576
27025
  {
26577
27026
  label: "Board:",
@@ -26587,7 +27036,7 @@ var App = () => {
26587
27036
  onSelect: selectBoard
26588
27037
  }
26589
27038
  ) }),
26590
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { style: { display: "flex", gap: 8 }, children: selectedBoard?.swimlanes.map((swimlane) => /* @__PURE__ */ (0, import_jsx_runtime17.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)(
26591
27040
  SwimlaneColumn,
26592
27041
  {
26593
27042
  swimlane,
@@ -26607,7 +27056,7 @@ var App = () => {
26607
27056
  swimlane.id
26608
27057
  )) })
26609
27058
  ] }),
26610
- selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
27059
+ selectedIssue && state?.user && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26611
27060
  IssueDetails,
26612
27061
  {
26613
27062
  whoAmI: state.user,
@@ -26624,6 +27073,10 @@ var App = () => {
26624
27073
  onRemoveAssignee: removeIssueAssignee,
26625
27074
  onAddComment: addIssueComment,
26626
27075
  onDeleteComment: deleteIssueComment,
27076
+ attachments: attachmentsByIssueId[selectedIssue.id] ?? [],
27077
+ attachmentUploadStatus,
27078
+ onUploadAttachments: uploadIssueAttachments,
27079
+ onDeleteAttachment: deleteIssueAttachment,
26627
27080
  onReopenIssue: reopenIssue,
26628
27081
  onCloseIssue: closeIssue,
26629
27082
  knownTags: state.tags ?? [],
@@ -26633,7 +27086,7 @@ var App = () => {
26633
27086
  ]
26634
27087
  }
26635
27088
  ),
26636
- createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
27089
+ createIssueModal && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
26637
27090
  CreateIssueModal,
26638
27091
  {
26639
27092
  title: createIssueModal.title,
@@ -26648,14 +27101,14 @@ var App = () => {
26648
27101
  };
26649
27102
 
26650
27103
  // source/gui/client/main.tsx
26651
- var import_jsx_runtime18 = __toESM(require_jsx_runtime(), 1);
27104
+ var import_jsx_runtime20 = __toESM(require_jsx_runtime(), 1);
26652
27105
  (0, import_client.createRoot)(document.getElementById("root")).render(
26653
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(BrowserRouter, { children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(Routes, { children: [
26654
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Route, { path: "/", element: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(App, {}) }),
26655
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Route, { path: "/board/:boardId", element: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(App, {}) }),
26656
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Route, { path: "/board/:boardId/issue/:issueId", element: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(App, {}) }),
26657
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Route, { path: "/board/:boardId/:issueId", element: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(App, {}) }),
26658
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Route, { path: "*", element: /* @__PURE__ */ (0, import_jsx_runtime18.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 }) })
26659
27112
  ] }) })
26660
27113
  );
26661
27114
  /*! Bundled license information: