@flamingo-stack/openframe-frontend-core 0.0.216 → 0.0.217

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.
Files changed (103) hide show
  1. package/dist/{chunk-SMCG2CCC.cjs → chunk-6DCKL73F.cjs} +24 -24
  2. package/dist/{chunk-SMCG2CCC.cjs.map → chunk-6DCKL73F.cjs.map} +1 -1
  3. package/dist/{chunk-QTKU6ULP.js → chunk-BVFRD34B.js} +2 -2
  4. package/dist/{chunk-CDLYRFDE.js → chunk-ENBGG2K2.js} +3767 -3610
  5. package/dist/chunk-ENBGG2K2.js.map +1 -0
  6. package/dist/{chunk-K4DFAVSO.cjs → chunk-G2HHSZ3S.cjs} +9 -9
  7. package/dist/{chunk-K4DFAVSO.cjs.map → chunk-G2HHSZ3S.cjs.map} +1 -1
  8. package/dist/{chunk-2V4SACHE.js → chunk-L6IBKPVM.js} +2 -2
  9. package/dist/{chunk-572WQWIX.cjs → chunk-MVQ3OODK.cjs} +9 -9
  10. package/dist/{chunk-572WQWIX.cjs.map → chunk-MVQ3OODK.cjs.map} +1 -1
  11. package/dist/{chunk-GVNQAGXB.js → chunk-N5IKPYRL.js} +3 -81
  12. package/dist/chunk-N5IKPYRL.js.map +1 -0
  13. package/dist/{chunk-VC3ND5RB.js → chunk-SWZUZYWR.js} +2 -2
  14. package/dist/{chunk-IH76P5R6.cjs → chunk-TYIBMDUZ.cjs} +8 -86
  15. package/dist/chunk-TYIBMDUZ.cjs.map +1 -0
  16. package/dist/{chunk-ZGTDUPTW.cjs → chunk-YWDC5BXM.cjs} +382 -225
  17. package/dist/chunk-YWDC5BXM.cjs.map +1 -0
  18. package/dist/components/chat/chat-attachment-bar.d.ts +13 -2
  19. package/dist/components/chat/chat-attachment-bar.d.ts.map +1 -1
  20. package/dist/components/chat/chat-input.d.ts.map +1 -1
  21. package/dist/components/chat/chat-message-row.d.ts +25 -0
  22. package/dist/components/chat/chat-message-row.d.ts.map +1 -0
  23. package/dist/components/chat/index.cjs +6 -2
  24. package/dist/components/chat/index.cjs.map +1 -1
  25. package/dist/components/chat/index.d.ts +1 -0
  26. package/dist/components/chat/index.d.ts.map +1 -1
  27. package/dist/components/chat/index.js +5 -1
  28. package/dist/components/chat/types/component.types.d.ts +8 -1
  29. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  30. package/dist/components/contact/index.cjs +3 -3
  31. package/dist/components/contact/index.js +2 -2
  32. package/dist/components/features/index.cjs +2 -2
  33. package/dist/components/features/index.js +1 -1
  34. package/dist/components/form.d.ts +1 -1
  35. package/dist/components/form.d.ts.map +1 -1
  36. package/dist/components/index.cjs +56 -52
  37. package/dist/components/index.cjs.map +1 -1
  38. package/dist/components/index.js +9 -5
  39. package/dist/components/index.js.map +1 -1
  40. package/dist/components/navigation/index.cjs +2 -2
  41. package/dist/components/navigation/index.js +1 -1
  42. package/dist/components/onboarding-guides/index.cjs +18 -18
  43. package/dist/components/onboarding-guides/index.js +3 -3
  44. package/dist/components/shared/dev-section/dev-card-row.d.ts +5 -45
  45. package/dist/components/shared/dev-section/dev-card-row.d.ts.map +1 -1
  46. package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -1
  47. package/dist/components/tickets/help-center-card.d.ts.map +1 -1
  48. package/dist/components/tickets/help-center-list.d.ts.map +1 -1
  49. package/dist/components/tickets/hooks/use-ticket-engagements.d.ts +9 -1
  50. package/dist/components/tickets/hooks/use-ticket-engagements.d.ts.map +1 -1
  51. package/dist/components/tickets/hooks/use-tickets-list.d.ts +7 -0
  52. package/dist/components/tickets/hooks/use-tickets-list.d.ts.map +1 -1
  53. package/dist/components/tickets/index.cjs +294 -255
  54. package/dist/components/tickets/index.cjs.map +1 -1
  55. package/dist/components/tickets/index.d.ts +1 -0
  56. package/dist/components/tickets/index.d.ts.map +1 -1
  57. package/dist/components/tickets/index.js +360 -321
  58. package/dist/components/tickets/index.js.map +1 -1
  59. package/dist/components/tickets/ticket-detail-drawer.d.ts.map +1 -1
  60. package/dist/components/tickets/ticket-reply-composer.d.ts +33 -0
  61. package/dist/components/tickets/ticket-reply-composer.d.ts.map +1 -0
  62. package/dist/components/tickets/types.d.ts +13 -0
  63. package/dist/components/tickets/types.d.ts.map +1 -1
  64. package/dist/components/ui/index.cjs +6 -2
  65. package/dist/components/ui/index.cjs.map +1 -1
  66. package/dist/components/ui/index.js +5 -1
  67. package/dist/components/ui/ticket-attachments-list.d.ts +5 -1
  68. package/dist/components/ui/ticket-attachments-list.d.ts.map +1 -1
  69. package/dist/index.cjs +6 -2
  70. package/dist/index.cjs.map +1 -1
  71. package/dist/index.js +5 -1
  72. package/dist/utils/index.cjs +59 -4
  73. package/dist/utils/index.cjs.map +1 -1
  74. package/dist/utils/index.js +59 -4
  75. package/dist/utils/index.js.map +1 -1
  76. package/dist/utils/scroll-into-view.d.ts +43 -48
  77. package/dist/utils/scroll-into-view.d.ts.map +1 -1
  78. package/package.json +1 -1
  79. package/src/components/chat/chat-attachment-bar.tsx +58 -22
  80. package/src/components/chat/chat-input.tsx +68 -29
  81. package/src/components/chat/chat-message-row.tsx +124 -0
  82. package/src/components/chat/index.ts +1 -0
  83. package/src/components/chat/types/component.types.ts +8 -1
  84. package/src/components/shared/dev-section/dev-card-row.tsx +5 -183
  85. package/src/components/shared/legal-document/use-legal-docs.ts +5 -1
  86. package/src/components/tickets/help-center-card.tsx +26 -29
  87. package/src/components/tickets/help-center-list.tsx +57 -10
  88. package/src/components/tickets/hooks/use-ticket-engagements.ts +17 -1
  89. package/src/components/tickets/hooks/use-tickets-list.ts +13 -0
  90. package/src/components/tickets/index.ts +4 -0
  91. package/src/components/tickets/ticket-detail-drawer.tsx +144 -200
  92. package/src/components/tickets/ticket-reply-composer.tsx +195 -0
  93. package/src/components/tickets/types.ts +14 -0
  94. package/src/components/ui/ticket-attachments-list.tsx +26 -8
  95. package/src/styles/app-globals.css +13 -0
  96. package/src/utils/scroll-into-view.ts +127 -53
  97. package/dist/chunk-CDLYRFDE.js.map +0 -1
  98. package/dist/chunk-GVNQAGXB.js.map +0 -1
  99. package/dist/chunk-IH76P5R6.cjs.map +0 -1
  100. package/dist/chunk-ZGTDUPTW.cjs.map +0 -1
  101. /package/dist/{chunk-QTKU6ULP.js.map → chunk-BVFRD34B.js.map} +0 -0
  102. /package/dist/{chunk-2V4SACHE.js.map → chunk-L6IBKPVM.js.map} +0 -0
  103. /package/dist/{chunk-VC3ND5RB.js.map → chunk-SWZUZYWR.js.map} +0 -0
@@ -4,18 +4,16 @@ import {
4
4
  EmptyState,
5
5
  UnifiedPagination,
6
6
  init_unified_pagination
7
- } from "../../chunk-2V4SACHE.js";
7
+ } from "../../chunk-L6IBKPVM.js";
8
8
  import {
9
- ConversationCardRow,
10
- ConversationCardRowSkeletonList,
11
9
  DevCardRowContent,
12
10
  DevCardRowSkeletonList,
13
11
  DevSectionPage
14
- } from "../../chunk-GVNQAGXB.js";
12
+ } from "../../chunk-N5IKPYRL.js";
15
13
  import "../../chunk-ORJREQ2W.js";
16
14
  import {
17
15
  ContactForm
18
- } from "../../chunk-QTKU6ULP.js";
16
+ } from "../../chunk-BVFRD34B.js";
19
17
  import {
20
18
  AlertDialog,
21
19
  AlertDialogAction,
@@ -28,18 +26,22 @@ import {
28
26
  Card,
29
27
  ChatAttachmentAddButton,
30
28
  ChatAttachmentChipStrip,
29
+ ChatInput,
30
+ ChatMessageRow,
31
+ ChatMessageRowSkeleton,
31
32
  ChatTicketItem,
32
33
  Label,
33
34
  SquareAvatar,
34
35
  StatusBadge,
35
36
  Textarea,
37
+ TicketAttachmentsList,
36
38
  embedAuthedFetch,
37
39
  formatRelativeTime,
38
40
  getStatusColorScheme,
39
41
  scrollElementIntoView,
40
42
  useChatAttachments,
41
43
  useChatIdentity
42
- } from "../../chunk-CDLYRFDE.js";
44
+ } from "../../chunk-ENBGG2K2.js";
43
45
  import "../../chunk-LXC6P2EO.js";
44
46
  import "../../chunk-EL5YVPD5.js";
45
47
  import {
@@ -68,7 +70,7 @@ import "../../chunk-CZR7ARBA.js";
68
70
  import "../../chunk-GGWZFCYS.js";
69
71
 
70
72
  // src/components/tickets/ticket-center.tsx
71
- import { useCallback as useCallback3, useState as useState4 } from "react";
73
+ import { useCallback as useCallback4, useState as useState4 } from "react";
72
74
  import { useQueryClient as useQueryClient2 } from "@tanstack/react-query";
73
75
  init_button();
74
76
  import { RefreshCw } from "lucide-react";
@@ -82,6 +84,7 @@ function isOptimistic(t) {
82
84
  return t._optimistic === true;
83
85
  }
84
86
  var TICKET_TEXT_MAX_CHARS = 5e3;
87
+ var TICKET_LIVE_POLL_MS = 8e3;
85
88
  var TOAST_COPY = {
86
89
  open_success: { title: "Ticket opened", description: "We received your message and will follow up shortly." },
87
90
  open_mirror_pending: { title: "Ticket opened", description: "Syncing \u2014 your ticket will appear momentarily." },
@@ -217,7 +220,7 @@ function TicketOpenForm({
217
220
  }
218
221
 
219
222
  // src/components/tickets/ticket-row.tsx
220
- import { useCallback, useRef } from "react";
223
+ import { useCallback as useCallback2, useRef } from "react";
221
224
 
222
225
  // src/components/collapsible.tsx
223
226
  import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
@@ -226,12 +229,12 @@ var CollapsibleContent2 = CollapsiblePrimitive.CollapsibleContent;
226
229
 
227
230
  // src/components/tickets/ticket-detail-drawer.tsx
228
231
  init_button();
229
- import { useState as useState2 } from "react";
232
+ import { useStickToBottom } from "use-stick-to-bottom";
230
233
 
231
234
  // src/components/tickets/hooks/use-ticket-engagements.ts
232
235
  import { useQuery } from "@tanstack/react-query";
233
236
  var LIST_ENGAGEMENTS_ENDPOINT = "/api/chat/agent/list-engagements";
234
- function useTicketEngagements(externalTicketId, enabled = true) {
237
+ function useTicketEngagements(externalTicketId, enabled = true, refetchInterval = false) {
235
238
  const identity = useChatIdentity();
236
239
  const identityKey = identity.user?.email ?? "anon";
237
240
  const queryEnabled = enabled && identity.authTier !== "anon" && !!identity.user?.email && !!externalTicketId && !externalTicketId.startsWith("temp-");
@@ -245,6 +248,11 @@ function useTicketEngagements(externalTicketId, enabled = true) {
245
248
  gcTime: 0,
246
249
  refetchOnMount: "always",
247
250
  refetchOnWindowFocus: true,
251
+ // Live conversation: poll while the caller opts in (drawer open). New
252
+ // agent replies + attachments appear within one interval without a
253
+ // manual refresh. `refetchIntervalInBackground` stays false (default)
254
+ // so polling pauses on a hidden tab.
255
+ refetchInterval,
248
256
  queryFn: async () => {
249
257
  const response = await embedAuthedFetch(LIST_ENGAGEMENTS_ENDPOINT, {
250
258
  method: "POST",
@@ -305,8 +313,134 @@ function TicketLinkedDeliveryCard({
305
313
  );
306
314
  }
307
315
 
308
- // src/components/tickets/ticket-detail-drawer.tsx
316
+ // src/components/tickets/ticket-reply-composer.tsx
317
+ init_button();
318
+ import { useCallback, useState as useState2 } from "react";
309
319
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
320
+ function TicketReplyComposer({
321
+ ticket,
322
+ busy,
323
+ supportSystemDown,
324
+ onSendMessage,
325
+ onClose
326
+ }) {
327
+ const [resolution, setResolution] = useState2("");
328
+ const [closeDialogOpen, setCloseDialogOpen] = useState2(false);
329
+ const attachments = useChatAttachments();
330
+ const ticketRef = { id: ticket.id, external_id: ticket.external_id };
331
+ const hasReadyFiles = attachments.readyAttachments.length > 0;
332
+ const handleSend = useCallback(
333
+ async (text) => {
334
+ const ref = { id: ticket.id, external_id: ticket.external_id };
335
+ const ok = await onSendMessage(ref, text.trim(), attachments.readyAttachments);
336
+ if (ok) attachments.clear();
337
+ return ok;
338
+ },
339
+ // Depend on the reactive projections, not the whole bag (a fresh object each
340
+ // render). `readyAttachments` is memo-stable; `clear` is callback-stable.
341
+ [
342
+ onSendMessage,
343
+ ticket.id,
344
+ ticket.external_id,
345
+ attachments.readyAttachments,
346
+ attachments.clear
347
+ ]
348
+ );
349
+ const confirmClose = async () => {
350
+ setCloseDialogOpen(false);
351
+ await onClose(ticketRef, resolution.trim() || void 0);
352
+ setResolution("");
353
+ };
354
+ const disabled = busy || supportSystemDown;
355
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2", children: [
356
+ /* @__PURE__ */ jsx3(
357
+ ChatAttachmentChipStrip,
358
+ {
359
+ attachments: attachments.attachments,
360
+ onRemove: attachments.removeAttachment,
361
+ disabled,
362
+ size: "compact"
363
+ }
364
+ ),
365
+ /* @__PURE__ */ jsx3(
366
+ ChatInput,
367
+ {
368
+ fullWidth: true,
369
+ autoFocus: true,
370
+ placeholder: "Type a reply\u2026",
371
+ sending: busy || attachments.hasInflightUploads,
372
+ disabled: supportSystemDown,
373
+ allowEmptySend: hasReadyFiles,
374
+ maxLength: TICKET_TEXT_MAX_CHARS,
375
+ onSend: handleSend
376
+ }
377
+ ),
378
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 w-full", children: [
379
+ !supportSystemDown && /* @__PURE__ */ jsx3(
380
+ ChatAttachmentAddButton,
381
+ {
382
+ attachmentsEnabled: true,
383
+ attachmentsCount: attachments.attachments.length,
384
+ onAddFiles: attachments.addFiles,
385
+ disabled,
386
+ size: "compact"
387
+ }
388
+ ),
389
+ /* @__PURE__ */ jsx3("div", { className: "flex-1 min-w-0" }),
390
+ /* @__PURE__ */ jsx3(
391
+ Button,
392
+ {
393
+ type: "button",
394
+ variant: "transparent",
395
+ size: "small",
396
+ onClick: () => setCloseDialogOpen(true),
397
+ disabled,
398
+ className: "text-ods-text-secondary hover:text-ods-text-primary",
399
+ children: "Close ticket"
400
+ }
401
+ )
402
+ ] }),
403
+ /* @__PURE__ */ jsx3(AlertDialog, { open: closeDialogOpen, onOpenChange: setCloseDialogOpen, children: /* @__PURE__ */ jsxs2(AlertDialogContent, { className: "bg-ods-card border-ods-border", children: [
404
+ /* @__PURE__ */ jsxs2(AlertDialogHeader, { children: [
405
+ /* @__PURE__ */ jsx3(AlertDialogTitle, { className: "text-ods-text-primary", children: "Close this ticket?" }),
406
+ /* @__PURE__ */ jsx3(AlertDialogDescription, { className: "text-ods-text-secondary", children: "Add an optional resolution note below. You can reopen the ticket later if needed." })
407
+ ] }),
408
+ /* @__PURE__ */ jsx3(
409
+ Textarea,
410
+ {
411
+ value: resolution,
412
+ onChange: (e) => setResolution(e.target.value),
413
+ placeholder: "Resolution (optional)",
414
+ rows: 3,
415
+ maxLength: TICKET_TEXT_MAX_CHARS,
416
+ className: "mt-2"
417
+ }
418
+ ),
419
+ /* @__PURE__ */ jsxs2(AlertDialogFooter, { children: [
420
+ /* @__PURE__ */ jsx3(
421
+ AlertDialogCancel,
422
+ {
423
+ disabled: busy,
424
+ className: "bg-transparent border-ods-border text-ods-text-primary hover:bg-ods-border",
425
+ children: "Cancel"
426
+ }
427
+ ),
428
+ /* @__PURE__ */ jsx3(
429
+ AlertDialogAction,
430
+ {
431
+ onClick: () => void confirmClose(),
432
+ disabled: busy,
433
+ className: "bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover",
434
+ children: "Close ticket"
435
+ }
436
+ )
437
+ ] })
438
+ ] }) })
439
+ ] });
440
+ }
441
+
442
+ // src/components/tickets/ticket-detail-drawer.tsx
443
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
310
444
  function TicketDetailDrawer({
311
445
  ticket,
312
446
  busy,
@@ -319,22 +453,22 @@ function TicketDetailDrawer({
319
453
  onClearReplyError
320
454
  }) {
321
455
  const isClosed = (ticket.status ?? "").toUpperCase() === "CLOSED";
322
- return /* @__PURE__ */ jsxs2("div", { className: "bg-ods-card border-t border-ods-border px-4 py-4 flex flex-col gap-4", children: [
323
- /* @__PURE__ */ jsx3(AssignedAgentRow, { assignedOwner: ticket.assignedOwner }),
324
- ticket.clickup && /* @__PURE__ */ jsx3(TicketLinkedDeliveryCard, { clickup: ticket.clickup }),
325
- /* @__PURE__ */ jsxs2("div", { children: [
326
- /* @__PURE__ */ jsx3("p", { className: "text-xs font-medium text-ods-text-secondary mb-2 uppercase tracking-wider", children: "Conversation" }),
327
- /* @__PURE__ */ jsx3(TicketTimelinePanel, { ticket })
456
+ return /* @__PURE__ */ jsxs3("div", { className: "bg-ods-card border-t border-ods-border px-4 py-4 flex flex-col gap-4", children: [
457
+ /* @__PURE__ */ jsx4(AssignedAgentRow, { assignedOwner: ticket.assignedOwner }),
458
+ ticket.clickup && /* @__PURE__ */ jsx4(TicketLinkedDeliveryCard, { clickup: ticket.clickup }),
459
+ /* @__PURE__ */ jsxs3("div", { children: [
460
+ /* @__PURE__ */ jsx4("p", { className: "text-xs font-medium text-ods-text-secondary mb-2 uppercase tracking-wider", children: "Conversation" }),
461
+ /* @__PURE__ */ jsx4(TicketTimelinePanel, { ticket })
328
462
  ] }),
329
- /* @__PURE__ */ jsxs2("div", { className: "border-t border-ods-border pt-4", children: [
330
- replyError && /* @__PURE__ */ jsx3(
463
+ /* @__PURE__ */ jsxs3("div", { className: "border-t border-ods-border pt-4", children: [
464
+ replyError && /* @__PURE__ */ jsx4(
331
465
  ReplyFailureBanner,
332
466
  {
333
467
  error: replyError,
334
468
  onDismiss: onClearReplyError ?? (() => void 0)
335
469
  }
336
470
  ),
337
- isClosed ? /* @__PURE__ */ jsx3(
471
+ isClosed ? /* @__PURE__ */ jsx4(
338
472
  ReopenAction,
339
473
  {
340
474
  ticketRef: { id: ticket.id, external_id: ticket.external_id },
@@ -343,37 +477,49 @@ function TicketDetailDrawer({
343
477
  onReopen,
344
478
  onActionCollapsed
345
479
  }
346
- ) : /* @__PURE__ */ jsx3(
347
- OpenActions,
480
+ ) : /* @__PURE__ */ jsx4(
481
+ TicketReplyComposer,
348
482
  {
349
483
  ticket,
350
484
  busy,
351
485
  supportSystemDown,
352
486
  onSendMessage,
353
- onClose,
354
- onActionCollapsed
487
+ onClose
355
488
  }
356
489
  )
357
490
  ] })
358
491
  ] });
359
492
  }
360
493
  var TURN_SEPARATOR_RE = /[\s]{1,16}---[\s]{1,16}/g;
494
+ var TICKET_FEED_FRAME = "bg-ods-card border border-ods-border rounded-[6px] overflow-y-auto w-full";
495
+ var TICKET_FEED_HEIGHT = "h-[60vh] md:h-[420px]";
496
+ var TICKET_FEED_INNER = "flex flex-col gap-4 md:gap-6 px-4 md:px-6 py-4 md:py-6";
497
+ var TICKET_FEED_SKELETON_ROWS = 6;
361
498
  function TicketTimelinePanel({ ticket }) {
362
499
  const identity = useChatIdentity();
363
500
  const externalId = isOptimistic(ticket) ? null : ticket.external_id;
364
- const { engagements, isLoading } = useTicketEngagements(externalId, !!externalId);
501
+ const { engagements, isLoading } = useTicketEngagements(
502
+ externalId,
503
+ !!externalId,
504
+ TICKET_LIVE_POLL_MS
505
+ );
506
+ const { scrollRef, contentRef } = useStickToBottom({ initial: "instant", resize: "smooth" });
365
507
  const bodyTurns = ticket.body ? ticket.body.split(TURN_SEPARATOR_RE).map((t) => t.trim()).filter(Boolean) : [];
508
+ const customerEngagementBodies = new Set(
509
+ engagements.filter((e) => e.authorRole === "customer").map((e) => (e.body ?? "").trim()).filter(Boolean)
510
+ );
511
+ const suppressBodyTurnZero = bodyTurns.length > 0 && customerEngagementBodies.has(bodyTurns[0]);
366
512
  const sessionEmailLower = identity.user?.email?.trim().toLowerCase() ?? null;
367
513
  const isViewerTheCustomer = !!sessionEmailLower && ticket.customer_emails.some((e) => e.trim().toLowerCase() === sessionEmailLower);
368
514
  const viewerName = identity.user?.name?.trim() || null;
369
515
  const ticketCustomerName = ticket.customer_name?.trim() || null;
370
516
  const customerName = (isViewerTheCustomer ? viewerName : null) || ticketCustomerName || viewerName || identity.user?.email || "You";
371
517
  const customerAvatar = isViewerTheCustomer ? identity.user?.avatarUrl ?? void 0 : void 0;
518
+ if (isLoading) {
519
+ return /* @__PURE__ */ jsx4("div", { className: `${TICKET_FEED_FRAME} ${TICKET_FEED_HEIGHT}`, children: /* @__PURE__ */ jsx4("div", { className: TICKET_FEED_INNER, children: Array.from({ length: TICKET_FEED_SKELETON_ROWS }, (_, i) => /* @__PURE__ */ jsx4(ChatMessageRowSkeleton, {}, i)) }) });
520
+ }
372
521
  if (bodyTurns.length === 0 && engagements.length === 0) {
373
- if (isLoading) {
374
- return /* @__PURE__ */ jsx3(ConversationCardRowSkeletonList, { rows: 2 });
375
- }
376
- return /* @__PURE__ */ jsx3(
522
+ return /* @__PURE__ */ jsx4(
377
523
  EmptyState,
378
524
  {
379
525
  type: "generic",
@@ -383,19 +529,17 @@ function TicketTimelinePanel({ ticket }) {
383
529
  }
384
530
  );
385
531
  }
386
- return /* @__PURE__ */ jsxs2("div", { className: "bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full", children: [
532
+ return /* @__PURE__ */ jsx4("div", { ref: scrollRef, className: `${TICKET_FEED_FRAME} ${TICKET_FEED_HEIGHT}`, children: /* @__PURE__ */ jsxs3("div", { ref: contentRef, className: TICKET_FEED_INNER, children: [
387
533
  bodyTurns.map((turn, i) => {
534
+ if (i === 0 && suppressBodyTurnZero) return null;
388
535
  const isResolution = turn.startsWith("[Resolution]");
389
- const role = i === 0 ? "Original message" : isResolution ? "Resolution" : `Update ${i}`;
390
536
  const text = isResolution ? turn.replace(/^\[Resolution\]\s*/, "") : turn;
391
- return /* @__PURE__ */ jsx3(
392
- ConversationCardRow,
537
+ return /* @__PURE__ */ jsx4(
538
+ ChatMessageRow,
393
539
  {
394
- author: customerName,
395
- role,
396
- avatarSrc: customerAvatar,
397
- body: text,
398
- variant: "current-user"
540
+ displayName: customerName,
541
+ avatarUrl: customerAvatar,
542
+ body: text
399
543
  },
400
544
  `body-${i}-${turn.slice(0, 24)}`
401
545
  );
@@ -418,32 +562,30 @@ function TicketTimelinePanel({ ticket }) {
418
562
  author = "Support team";
419
563
  avatarSrc = void 0;
420
564
  }
421
- return /* @__PURE__ */ jsx3(
422
- ConversationCardRow,
565
+ const engAttachments = mapEngagementAttachments(eng.attachments);
566
+ return /* @__PURE__ */ jsx4(
567
+ ChatMessageRow,
423
568
  {
424
- author,
425
- role: "Reply",
426
- avatarSrc,
427
- timestamp: eng.createdAt,
569
+ displayName: author,
570
+ avatarUrl: avatarSrc,
571
+ timeLabel: eng.createdAt ? formatRelativeTime(eng.createdAt) : null,
428
572
  body: stripAttachmentsPreamble(eng.body ?? ""),
429
- attachments: mapEngagementAttachments(eng.attachments),
430
- variant: isCustomer ? "current-user" : "support"
573
+ footer: engAttachments.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "mt-2", children: /* @__PURE__ */ jsx4(TicketAttachmentsList, { attachments: engAttachments, size: "compact" }) }) : null
431
574
  },
432
575
  eng.id
433
576
  );
434
- }),
435
- isLoading && // Trailing single-row skeleton when the panel already has
436
- // content rendered — drawer is showing the customer's body
437
- // turns + cached engagements while a background refetch is
438
- // in flight. Single row keeps the placeholder modest.
439
- /* @__PURE__ */ jsx3(ConversationCardRowSkeletonList, { rows: 1 })
440
- ] });
577
+ })
578
+ ] }) });
441
579
  }
442
580
  function mapEngagementAttachments(files) {
443
581
  return files.map((f) => ({
444
582
  id: f.id,
445
583
  fileName: f.name ?? `file-${f.id}`,
446
584
  fileSize: f.size ? formatBytes(f.size) : "",
585
+ // Show an inline thumbnail for image attachments (the signed `url` is a
586
+ // viewable URL). Non-images fall back to the file-type icon. SquareAvatar
587
+ // degrades to initials on a broken/expired image URL.
588
+ thumbnailSrc: f.url && (f.mime?.startsWith("image/") ?? false) ? f.url : void 0,
447
589
  onDownload: f.url ? () => window.open(f.url, "_blank", "noopener,noreferrer") : void 0
448
590
  }));
449
591
  }
@@ -466,10 +608,12 @@ function ReopenAction({
466
608
  const handleReopen = async () => {
467
609
  void await onReopen(ticketRef);
468
610
  };
469
- return /* @__PURE__ */ jsx3("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx3(
611
+ return /* @__PURE__ */ jsx4("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx4(
470
612
  Button,
471
613
  {
472
614
  type: "button",
615
+ variant: "outline",
616
+ size: "small",
473
617
  onClick: () => void handleReopen(),
474
618
  disabled: busy || supportSystemDown,
475
619
  loading: busy,
@@ -477,146 +621,19 @@ function ReopenAction({
477
621
  }
478
622
  ) });
479
623
  }
480
- function OpenActions({
481
- ticket,
482
- busy,
483
- supportSystemDown,
484
- onSendMessage,
485
- onClose,
486
- onActionCollapsed
487
- }) {
488
- const [messageText, setMessageText] = useState2("");
489
- const [resolution, setResolution] = useState2("");
490
- const [closeDialogOpen, setCloseDialogOpen] = useState2(false);
491
- const attachments = useChatAttachments();
492
- const disabled = busy || supportSystemDown;
493
- const ticketRef = { id: ticket.id, external_id: ticket.external_id };
494
- const hasText = messageText.trim().length > 0;
495
- const hasReadyFiles = attachments.readyAttachments.length > 0;
496
- const canSend = !disabled && (hasText || hasReadyFiles) && !attachments.hasInflightUploads;
497
- const sendMessage = async () => {
498
- if (!canSend) return;
499
- const ok = await onSendMessage(ticketRef, messageText.trim(), attachments.readyAttachments);
500
- if (ok) {
501
- setMessageText("");
502
- attachments.clear();
503
- }
504
- };
505
- const confirmClose = async () => {
506
- setCloseDialogOpen(false);
507
- await onClose(ticketRef, resolution.trim() || void 0);
508
- setResolution("");
509
- };
510
- return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-3", children: [
511
- /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2", children: [
512
- /* @__PURE__ */ jsx3(
513
- Textarea,
514
- {
515
- value: messageText,
516
- onChange: (e) => setMessageText(e.target.value),
517
- placeholder: "Type a reply\u2026 (attach files if needed)",
518
- disabled,
519
- rows: 3,
520
- maxLength: TICKET_TEXT_MAX_CHARS
521
- }
522
- ),
523
- /* @__PURE__ */ jsx3(
524
- ChatAttachmentChipStrip,
525
- {
526
- attachments: attachments.attachments,
527
- onRemove: attachments.removeAttachment,
528
- disabled
529
- }
530
- ),
531
- /* @__PURE__ */ jsxs2("div", { className: "flex justify-between items-center gap-2 flex-wrap", children: [
532
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
533
- /* @__PURE__ */ jsx3(
534
- ChatAttachmentAddButton,
535
- {
536
- attachmentsEnabled: !supportSystemDown,
537
- attachmentsCount: attachments.attachments.length,
538
- onAddFiles: attachments.addFiles,
539
- disabled
540
- }
541
- ),
542
- /* @__PURE__ */ jsx3("span", { className: "text-xs text-ods-text-secondary", children: "Attach files" })
543
- ] }),
544
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
545
- /* @__PURE__ */ jsx3(
546
- Button,
547
- {
548
- type: "button",
549
- variant: "transparent",
550
- onClick: () => setCloseDialogOpen(true),
551
- disabled,
552
- className: "bg-ods-error hover:bg-ods-error-hover text-white border-transparent",
553
- children: "Close ticket"
554
- }
555
- ),
556
- /* @__PURE__ */ jsx3(
557
- Button,
558
- {
559
- type: "button",
560
- onClick: () => void sendMessage(),
561
- disabled: !canSend,
562
- loading: busy,
563
- children: "Send reply"
564
- }
565
- )
566
- ] })
567
- ] })
568
- ] }),
569
- /* @__PURE__ */ jsx3(AlertDialog, { open: closeDialogOpen, onOpenChange: setCloseDialogOpen, children: /* @__PURE__ */ jsxs2(AlertDialogContent, { className: "bg-ods-card border-ods-border", children: [
570
- /* @__PURE__ */ jsxs2(AlertDialogHeader, { children: [
571
- /* @__PURE__ */ jsx3(AlertDialogTitle, { className: "text-ods-text-primary font-['DM_Sans'] text-[20px] font-semibold", children: "Close this ticket?" }),
572
- /* @__PURE__ */ jsx3(AlertDialogDescription, { className: "text-ods-text-secondary font-['DM_Sans'] text-[14px]", children: "Add an optional resolution note below. You can reopen the ticket later if needed." })
573
- ] }),
574
- /* @__PURE__ */ jsx3(
575
- Textarea,
576
- {
577
- value: resolution,
578
- onChange: (e) => setResolution(e.target.value),
579
- placeholder: "Resolution (optional)",
580
- rows: 3,
581
- maxLength: TICKET_TEXT_MAX_CHARS,
582
- className: "mt-2"
583
- }
584
- ),
585
- /* @__PURE__ */ jsxs2(AlertDialogFooter, { children: [
586
- /* @__PURE__ */ jsx3(
587
- AlertDialogCancel,
588
- {
589
- disabled: busy,
590
- className: "bg-transparent border-ods-border text-ods-text-primary hover:bg-ods-border",
591
- children: "Cancel"
592
- }
593
- ),
594
- /* @__PURE__ */ jsx3(
595
- AlertDialogAction,
596
- {
597
- onClick: () => void confirmClose(),
598
- disabled: busy,
599
- className: "bg-ods-error hover:bg-ods-error-hover text-white",
600
- children: "Close ticket"
601
- }
602
- )
603
- ] })
604
- ] }) })
605
- ] });
606
- }
607
624
  function ReplyFailureBanner({
608
625
  error,
609
626
  onDismiss
610
627
  }) {
611
- return /* @__PURE__ */ jsxs2(
628
+ return /* @__PURE__ */ jsxs3(
612
629
  "div",
613
630
  {
614
631
  role: "status",
615
632
  "aria-live": "polite",
616
633
  className: "mb-3 flex items-start gap-3 rounded-md border border-ods-attention-red-error bg-ods-attention-red-error-secondary px-3 py-2 text-sm text-ods-attention-red-error",
617
634
  children: [
618
- /* @__PURE__ */ jsx3("span", { className: "font-medium leading-snug", children: error.message }),
619
- /* @__PURE__ */ jsx3(
635
+ /* @__PURE__ */ jsx4("span", { className: "font-medium leading-snug", children: error.message }),
636
+ /* @__PURE__ */ jsx4(
620
637
  Button,
621
638
  {
622
639
  type: "button",
@@ -637,10 +654,10 @@ function AssignedAgentRow({
637
654
  const trimmedName = assignedOwner?.name?.trim() || null;
638
655
  const emailFallback = assignedOwner?.email?.trim() || null;
639
656
  const displayLabel = trimmedName ?? (emailFallback ? emailFallback.split("@")[0] : null);
640
- return /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 text-xs", children: [
641
- /* @__PURE__ */ jsx3("span", { className: "text-ods-text-secondary uppercase tracking-wider font-medium", children: "Assigned to" }),
642
- displayLabel ? /* @__PURE__ */ jsxs2("span", { className: "flex items-center gap-1.5 text-ods-text-primary font-medium", children: [
643
- /* @__PURE__ */ jsx3(
657
+ return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-xs", children: [
658
+ /* @__PURE__ */ jsx4("span", { className: "text-ods-text-secondary uppercase tracking-wider font-medium", children: "Assigned to" }),
659
+ displayLabel ? /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-1.5 text-ods-text-primary font-medium", children: [
660
+ /* @__PURE__ */ jsx4(
644
661
  SquareAvatar,
645
662
  {
646
663
  size: "sm",
@@ -651,12 +668,12 @@ function AssignedAgentRow({
651
668
  }
652
669
  ),
653
670
  displayLabel
654
- ] }) : /* @__PURE__ */ jsx3("span", { className: "text-ods-text-secondary italic", children: "Unassigned" })
671
+ ] }) : /* @__PURE__ */ jsx4("span", { className: "text-ods-text-secondary italic", children: "Unassigned" })
655
672
  ] });
656
673
  }
657
674
 
658
675
  // src/components/tickets/ticket-row.tsx
659
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
676
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
660
677
  function TicketRow({
661
678
  ticket,
662
679
  expanded,
@@ -670,7 +687,7 @@ function TicketRow({
670
687
  }) {
671
688
  const optimistic = isOptimistic(ticket);
672
689
  const rowRef = useRef(null);
673
- const handleClick = useCallback(() => {
690
+ const handleClick = useCallback2(() => {
674
691
  onToggle(ticket.id);
675
692
  scrollElementIntoView(rowRef.current, {
676
693
  adjustTargetY: (raw) => {
@@ -706,13 +723,13 @@ function TicketRow({
706
723
  // when the task exists but its status hasn't synced yet.
707
724
  linkedTaskLabel: ticket.clickup ? ticket.clickup.status ? ticket.clickup.status.replace(/\b\w/g, (c) => c.toUpperCase()) : "Linked work" : void 0
708
725
  };
709
- return /* @__PURE__ */ jsx4("div", { ref: rowRef, className: "scroll-mt-24", children: /* @__PURE__ */ jsxs3(
726
+ return /* @__PURE__ */ jsx5("div", { ref: rowRef, className: "scroll-mt-24", children: /* @__PURE__ */ jsxs4(
710
727
  Collapsible,
711
728
  {
712
729
  open: expanded && !optimistic,
713
730
  className: "border-b border-ods-border last:border-b-0",
714
731
  children: [
715
- /* @__PURE__ */ jsx4(
732
+ /* @__PURE__ */ jsx5(
716
733
  ChatTicketItem,
717
734
  {
718
735
  ticket: tileData,
@@ -721,12 +738,12 @@ function TicketRow({
721
738
  "aria-controls": `ticket-drawer-${ticket.id}`
722
739
  }
723
740
  ),
724
- /* @__PURE__ */ jsx4(
741
+ /* @__PURE__ */ jsx5(
725
742
  CollapsibleContent2,
726
743
  {
727
744
  id: `ticket-drawer-${ticket.id}`,
728
745
  className: "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
729
- children: /* @__PURE__ */ jsx4(
746
+ children: /* @__PURE__ */ jsx5(
730
747
  TicketDetailDrawer,
731
748
  {
732
749
  ticket,
@@ -758,6 +775,7 @@ function useTicketsList(filters) {
758
775
  const pageSize = Math.max(1, Math.min(100, Math.floor(filters.pageSize ?? DEFAULT_PAGE_SIZE) || DEFAULT_PAGE_SIZE));
759
776
  const enabled = !!customerEmail;
760
777
  const identityKey = customerEmail || "anon";
778
+ const refetchInterval = filters.refetchInterval ?? false;
761
779
  const query = useQuery2({
762
780
  queryKey: ["tickets", "self", identityKey, search, statusFilter, page, pageSize],
763
781
  enabled,
@@ -770,6 +788,10 @@ function useTicketsList(filters) {
770
788
  gcTime: 0,
771
789
  refetchOnMount: "always",
772
790
  refetchOnWindowFocus: true,
791
+ // Live status: poll while the caller opts in (drawer open). Defaults to
792
+ // false. `refetchIntervalInBackground` stays false (the default) so polling
793
+ // pauses on a hidden tab — no wasted requests when the user tabs away.
794
+ refetchInterval,
773
795
  queryFn: async () => {
774
796
  const body = {
775
797
  query: search,
@@ -831,7 +853,7 @@ function useTicketsList(filters) {
831
853
  }
832
854
 
833
855
  // src/components/tickets/hooks/use-ticket-actions.ts
834
- import { useCallback as useCallback2, useEffect, useMemo, useRef as useRef2, useState as useState3 } from "react";
856
+ import { useCallback as useCallback3, useEffect, useMemo, useRef as useRef2, useState as useState3 } from "react";
835
857
  import { useQueryClient } from "@tanstack/react-query";
836
858
  var TICKET_ACTION_ENDPOINT = "/api/chat/agent/ticket-action";
837
859
  var REPLY_BANNER_CODES = /* @__PURE__ */ new Set([
@@ -848,14 +870,14 @@ function useTicketActions(options) {
848
870
  const [isSubmittingForm, setIsSubmittingForm] = useState3(false);
849
871
  const busyRowsRef = useRef2(/* @__PURE__ */ new Set());
850
872
  const [busyRows, setBusyRows] = useState3(() => /* @__PURE__ */ new Set());
851
- const setRowBusy = useCallback2((id, busy) => {
873
+ const setRowBusy = useCallback3((id, busy) => {
852
874
  if (busy) busyRowsRef.current.add(id);
853
875
  else busyRowsRef.current.delete(id);
854
876
  setBusyRows(new Set(busyRowsRef.current));
855
877
  }, []);
856
- const isRowBusy = useCallback2((id) => busyRows.has(id), [busyRows]);
878
+ const isRowBusy = useCallback3((id) => busyRows.has(id), [busyRows]);
857
879
  const [replyErrorByTicket, setReplyErrorByTicket] = useState3(() => /* @__PURE__ */ new Map());
858
- const setReplyError = useCallback2(
880
+ const setReplyError = useCallback3(
859
881
  (externalId, mapped) => {
860
882
  setReplyErrorByTicket((prev) => {
861
883
  const next = new Map(prev);
@@ -866,11 +888,11 @@ function useTicketActions(options) {
866
888
  },
867
889
  []
868
890
  );
869
- const replyErrorFor = useCallback2(
891
+ const replyErrorFor = useCallback3(
870
892
  (externalId) => replyErrorByTicket.get(externalId) ?? null,
871
893
  [replyErrorByTicket]
872
894
  );
873
- const clearReplyError = useCallback2(
895
+ const clearReplyError = useCallback3(
874
896
  (externalId) => setReplyError(externalId, null),
875
897
  [setReplyError]
876
898
  );
@@ -884,12 +906,12 @@ function useTicketActions(options) {
884
906
  };
885
907
  }, []);
886
908
  const queueRef = useRef2(Promise.resolve());
887
- const enqueue = useCallback2((work) => {
909
+ const enqueue = useCallback3((work) => {
888
910
  const next = queueRef.current.then(work, work);
889
911
  queueRef.current = next.catch(() => void 0);
890
912
  return next;
891
913
  }, []);
892
- const executeTicketAction = useCallback2(
914
+ const executeTicketAction = useCallback3(
893
915
  async (toolName, args) => {
894
916
  const res = await embedAuthedFetch(TICKET_ACTION_ENDPOINT, {
895
917
  method: "POST",
@@ -905,7 +927,7 @@ function useTicketActions(options) {
905
927
  },
906
928
  []
907
929
  );
908
- const watchMirrorSync = useCallback2(
930
+ const watchMirrorSync = useCallback3(
909
931
  (placeholderId, expectedTicketId) => {
910
932
  const prior = watcherControllersRef.current.get(placeholderId);
911
933
  if (prior) prior.abort();
@@ -952,7 +974,7 @@ function useTicketActions(options) {
952
974
  [queryClient, removeOptimistic, toast2]
953
975
  );
954
976
  const lastUpdateErrorRef = useRef2(null);
955
- const surfaceError = useCallback2(
977
+ const surfaceError = useCallback3(
956
978
  (err, action) => {
957
979
  const mapped = mapTicketActionError(err);
958
980
  lastUpdateErrorRef.current = mapped;
@@ -966,7 +988,7 @@ function useTicketActions(options) {
966
988
  },
967
989
  [toast2, onSupportSystemDown]
968
990
  );
969
- const submitTicket = useCallback2(
991
+ const submitTicket = useCallback3(
970
992
  async (input) => {
971
993
  if (formInFlightRef.current) return false;
972
994
  formInFlightRef.current = true;
@@ -1035,7 +1057,7 @@ function useTicketActions(options) {
1035
1057
  surfaceError
1036
1058
  ]
1037
1059
  );
1038
- const updateTicket = useCallback2(
1060
+ const updateTicket = useCallback3(
1039
1061
  async (ticket, serverArgs, successCopy, action) => {
1040
1062
  if (busyRowsRef.current.has(ticket.id)) return false;
1041
1063
  setRowBusy(ticket.id, true);
@@ -1081,7 +1103,7 @@ function useTicketActions(options) {
1081
1103
  // and cascade-recreate addNote/closeTicket/etc.
1082
1104
  [setRowBusy, enqueue, executeTicketAction, queryClient, toast2, surfaceError, removeTicketFromCache]
1083
1105
  );
1084
- const sendMessage = useCallback2(
1106
+ const sendMessage = useCallback3(
1085
1107
  async (ticket, text, attachments) => {
1086
1108
  const trimmed = text.trim();
1087
1109
  const hasText = trimmed.length > 0;
@@ -1110,7 +1132,7 @@ function useTicketActions(options) {
1110
1132
  },
1111
1133
  [updateTicket, clearReplyError, setReplyError]
1112
1134
  );
1113
- const closeTicket = useCallback2(
1135
+ const closeTicket = useCallback3(
1114
1136
  (ticket, resolution) => updateTicket(
1115
1137
  ticket,
1116
1138
  {
@@ -1122,7 +1144,7 @@ function useTicketActions(options) {
1122
1144
  ),
1123
1145
  [updateTicket]
1124
1146
  );
1125
- const reopenTicket = useCallback2(
1147
+ const reopenTicket = useCallback3(
1126
1148
  (ticket) => updateTicket(ticket, { status: "OPEN" }, TOAST_COPY.reopen_success, "reopen ticket"),
1127
1149
  [updateTicket]
1128
1150
  );
@@ -1274,14 +1296,14 @@ function resolveErrorCode(bodyCode, status) {
1274
1296
  }
1275
1297
 
1276
1298
  // src/components/tickets/ticket-center.tsx
1277
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1299
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1278
1300
  function TicketCenter({ toast: toast2 = toast } = {}) {
1279
1301
  const identity = useChatIdentity();
1280
1302
  if (identity.isLoading) {
1281
- return /* @__PURE__ */ jsx5(TicketCenterSkeleton, {});
1303
+ return /* @__PURE__ */ jsx6(TicketCenterSkeleton, {});
1282
1304
  }
1283
1305
  if (identity.authTier === "anon" || !identity.user?.email) {
1284
- return /* @__PURE__ */ jsx5(
1306
+ return /* @__PURE__ */ jsx6(
1285
1307
  EmptyState,
1286
1308
  {
1287
1309
  type: "generic",
@@ -1291,7 +1313,7 @@ function TicketCenter({ toast: toast2 = toast } = {}) {
1291
1313
  }
1292
1314
  );
1293
1315
  }
1294
- return /* @__PURE__ */ jsx5(
1316
+ return /* @__PURE__ */ jsx6(
1295
1317
  TicketCenterAuthed,
1296
1318
  {
1297
1319
  toast: toast2,
@@ -1310,14 +1332,14 @@ function TicketCenterAuthed({
1310
1332
  const [optimisticTickets, setOptimisticTickets] = useState4([]);
1311
1333
  const [expandedTicketId, setExpandedTicketId] = useState4(null);
1312
1334
  const [supportSystemDown, setSupportSystemDown] = useState4(false);
1313
- const prependOptimistic = useCallback3((placeholder) => {
1335
+ const prependOptimistic = useCallback4((placeholder) => {
1314
1336
  setOptimisticTickets((prev) => [placeholder, ...prev]);
1315
1337
  }, []);
1316
- const removeOptimistic = useCallback3((placeholderId) => {
1338
+ const removeOptimistic = useCallback4((placeholderId) => {
1317
1339
  setOptimisticTickets((prev) => prev.filter((t) => t.id !== placeholderId));
1318
1340
  setExpandedTicketId((prev) => prev === placeholderId ? null : prev);
1319
1341
  }, []);
1320
- const removeTicketFromCache = useCallback3(
1342
+ const removeTicketFromCache = useCallback4(
1321
1343
  (ticketId) => {
1322
1344
  queryClient.setQueriesData(
1323
1345
  { queryKey: ["tickets"] },
@@ -1334,12 +1356,12 @@ function TicketCenterAuthed({
1334
1356
  toast: toast2,
1335
1357
  onSupportSystemDown: () => setSupportSystemDown(true)
1336
1358
  });
1337
- const toggleRow = useCallback3((id) => {
1359
+ const toggleRow = useCallback4((id) => {
1338
1360
  setExpandedTicketId((prev) => prev === id ? null : id);
1339
1361
  }, []);
1340
1362
  const merged = [...optimisticTickets, ...tickets];
1341
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-6", children: [
1342
- /* @__PURE__ */ jsx5(
1363
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-6", children: [
1364
+ /* @__PURE__ */ jsx6(
1343
1365
  TicketOpenForm,
1344
1366
  {
1345
1367
  onSubmit: (input) => actions.submitTicket(input),
@@ -1347,15 +1369,15 @@ function TicketCenterAuthed({
1347
1369
  supportSystemDown
1348
1370
  }
1349
1371
  ),
1350
- /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2", children: [
1351
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between gap-3", children: [
1352
- /* @__PURE__ */ jsx5("p", { className: "text-xs font-medium text-ods-text-secondary uppercase tracking-wider", children: "Your Current Tickets" }),
1353
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-3 text-xs text-ods-text-secondary", children: [
1354
- lastUpdatedAt && /* @__PURE__ */ jsxs4("span", { children: [
1372
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
1373
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between gap-3", children: [
1374
+ /* @__PURE__ */ jsx6("p", { className: "text-xs font-medium text-ods-text-secondary uppercase tracking-wider", children: "Your Current Tickets" }),
1375
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-3 text-xs text-ods-text-secondary", children: [
1376
+ lastUpdatedAt && /* @__PURE__ */ jsxs5("span", { children: [
1355
1377
  "Updated ",
1356
1378
  formatRelativeTime(new Date(lastUpdatedAt))
1357
1379
  ] }),
1358
- /* @__PURE__ */ jsx5(
1380
+ /* @__PURE__ */ jsx6(
1359
1381
  Button,
1360
1382
  {
1361
1383
  type: "button",
@@ -1364,12 +1386,12 @@ function TicketCenterAuthed({
1364
1386
  onClick: refetch,
1365
1387
  disabled: isFetching,
1366
1388
  "aria-label": "Refresh ticket list",
1367
- leftIcon: /* @__PURE__ */ jsx5(RefreshCw, { className: "h-4 w-4" })
1389
+ leftIcon: /* @__PURE__ */ jsx6(RefreshCw, { className: "h-4 w-4" })
1368
1390
  }
1369
1391
  )
1370
1392
  ] })
1371
1393
  ] }),
1372
- isLoading ? /* @__PURE__ */ jsx5(TicketListSkeleton, {}) : merged.length === 0 ? /* @__PURE__ */ jsx5(Card, { className: "p-6", children: /* @__PURE__ */ jsx5(
1394
+ isLoading ? /* @__PURE__ */ jsx6(TicketListSkeleton, {}) : merged.length === 0 ? /* @__PURE__ */ jsx6(Card, { className: "p-6", children: /* @__PURE__ */ jsx6(
1373
1395
  EmptyState,
1374
1396
  {
1375
1397
  type: "generic",
@@ -1377,7 +1399,7 @@ function TicketCenterAuthed({
1377
1399
  description: "Open one above to start the conversation.",
1378
1400
  showCTA: false
1379
1401
  }
1380
- ) }) : /* @__PURE__ */ jsx5(Card, { className: "overflow-hidden", children: merged.map((ticket) => /* @__PURE__ */ jsx5(
1402
+ ) }) : /* @__PURE__ */ jsx6(Card, { className: "overflow-hidden", children: merged.map((ticket) => /* @__PURE__ */ jsx6(
1381
1403
  TicketRow,
1382
1404
  {
1383
1405
  ticket,
@@ -1396,34 +1418,34 @@ function TicketCenterAuthed({
1396
1418
  ] });
1397
1419
  }
1398
1420
  function TicketCenterSkeleton() {
1399
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-6", children: [
1400
- /* @__PURE__ */ jsxs4(Card, { className: "p-6", children: [
1401
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-7 w-48 mb-4" }),
1402
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-10 w-full mb-3" }),
1403
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-24 w-full" })
1421
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-6", children: [
1422
+ /* @__PURE__ */ jsxs5(Card, { className: "p-6", children: [
1423
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-7 w-48 mb-4" }),
1424
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-10 w-full mb-3" }),
1425
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-24 w-full" })
1404
1426
  ] }),
1405
- /* @__PURE__ */ jsx5(TicketListSkeleton, {})
1427
+ /* @__PURE__ */ jsx6(TicketListSkeleton, {})
1406
1428
  ] });
1407
1429
  }
1408
1430
  function TicketListSkeleton() {
1409
- return /* @__PURE__ */ jsx5(Card, { className: "overflow-hidden", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs4("div", { className: "h-20 px-4 flex items-center gap-4 border-b border-ods-border last:border-b-0", children: [
1410
- /* @__PURE__ */ jsxs4("div", { className: "flex-1 flex flex-col gap-2", children: [
1411
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-4 w-2/3" }),
1412
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-3 w-full" })
1431
+ return /* @__PURE__ */ jsx6(Card, { className: "overflow-hidden", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs5("div", { className: "h-20 px-4 flex items-center gap-4 border-b border-ods-border last:border-b-0", children: [
1432
+ /* @__PURE__ */ jsxs5("div", { className: "flex-1 flex flex-col gap-2", children: [
1433
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-4 w-2/3" }),
1434
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-3 w-full" })
1413
1435
  ] }),
1414
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-8 w-20" }),
1415
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-8 w-16" })
1436
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-8 w-20" }),
1437
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-8 w-16" })
1416
1438
  ] }, i)) });
1417
1439
  }
1418
1440
 
1419
1441
  // src/components/tickets/help-center-list.tsx
1420
- import { useCallback as useCallback5, useState as useState6 } from "react";
1442
+ import { useCallback as useCallback6, useState as useState6 } from "react";
1421
1443
  import { useQueryClient as useQueryClient3 } from "@tanstack/react-query";
1422
1444
  init_unified_pagination();
1423
1445
 
1424
1446
  // src/components/tickets/help-center-card.tsx
1425
- import { useCallback as useCallback4, useRef as useRef3 } from "react";
1426
- import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1447
+ import { useCallback as useCallback5, useEffect as useEffect2, useRef as useRef3 } from "react";
1448
+ import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1427
1449
  var STICKY_HEADER_OFFSET_PX = 96;
1428
1450
  function HelpCenterCard({
1429
1451
  ticket,
@@ -1448,25 +1470,20 @@ function HelpCenterCard({
1448
1470
  const isExpandable = !optimistic;
1449
1471
  const isExpanded = expanded && isExpandable;
1450
1472
  const rowRef = useRef3(null);
1451
- const handleClick = useCallback4(() => {
1473
+ const handleClick = useCallback5(() => {
1452
1474
  onToggle(ticket.id);
1453
- scrollElementIntoView(rowRef.current, {
1454
- headerOffset: STICKY_HEADER_OFFSET_PX,
1455
- adjustTargetY: (raw) => {
1456
- if (!rowRef.current) return raw;
1457
- const expandedDrawer = document.querySelector(
1458
- 'div[id^="help-center-drawer-"]'
1459
- );
1460
- if (!(expandedDrawer instanceof HTMLElement)) return raw;
1461
- const drawerRect = expandedDrawer.getBoundingClientRect();
1462
- const myRect = rowRef.current.getBoundingClientRect();
1463
- if (drawerRect.bottom > myRect.top) return raw;
1464
- return raw - drawerRect.height;
1465
- }
1466
- });
1467
1475
  }, [onToggle, ticket.id]);
1468
- const rightBadges = /* @__PURE__ */ jsxs5(Fragment, { children: [
1469
- /* @__PURE__ */ jsx6(
1476
+ useEffect2(() => {
1477
+ if (!isExpanded) return;
1478
+ const raf = requestAnimationFrame(() => {
1479
+ scrollElementIntoView(rowRef.current, {
1480
+ headerOffset: STICKY_HEADER_OFFSET_PX
1481
+ });
1482
+ });
1483
+ return () => cancelAnimationFrame(raf);
1484
+ }, [isExpanded]);
1485
+ const rightBadges = /* @__PURE__ */ jsxs6(Fragment, { children: [
1486
+ /* @__PURE__ */ jsx7(
1470
1487
  StatusBadge,
1471
1488
  {
1472
1489
  text: rawStatus,
@@ -1475,7 +1492,7 @@ function HelpCenterCard({
1475
1492
  className: "border border-ods-border"
1476
1493
  }
1477
1494
  ),
1478
- priority && /* @__PURE__ */ jsx6(
1495
+ priority && /* @__PURE__ */ jsx7(
1479
1496
  StatusBadge,
1480
1497
  {
1481
1498
  text: priority,
@@ -1485,7 +1502,7 @@ function HelpCenterCard({
1485
1502
  }
1486
1503
  )
1487
1504
  ] });
1488
- return /* @__PURE__ */ jsxs5(
1505
+ return /* @__PURE__ */ jsxs6(
1489
1506
  "div",
1490
1507
  {
1491
1508
  ref: rowRef,
@@ -1493,7 +1510,7 @@ function HelpCenterCard({
1493
1510
  className: `border-b border-ods-border last:border-b-0 ${optimistic ? "opacity-60" : ""}`,
1494
1511
  "aria-busy": optimistic || void 0,
1495
1512
  children: [
1496
- /* @__PURE__ */ jsx6(
1513
+ /* @__PURE__ */ jsx7(
1497
1514
  "button",
1498
1515
  {
1499
1516
  type: "button",
@@ -1502,7 +1519,7 @@ function HelpCenterCard({
1502
1519
  "aria-expanded": isExpandable ? isExpanded : void 0,
1503
1520
  "aria-controls": isExpanded ? `help-center-drawer-${ticket.id}` : void 0,
1504
1521
  className: "w-full text-left p-[12px] md:p-[16px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-inset disabled:cursor-default",
1505
- children: /* @__PURE__ */ jsx6(
1522
+ children: /* @__PURE__ */ jsx7(
1506
1523
  DevCardRowContent,
1507
1524
  {
1508
1525
  title,
@@ -1514,7 +1531,7 @@ function HelpCenterCard({
1514
1531
  )
1515
1532
  }
1516
1533
  ),
1517
- isExpanded && /* @__PURE__ */ jsx6("div", { id: `help-center-drawer-${ticket.id}`, children: /* @__PURE__ */ jsx6(
1534
+ isExpanded && /* @__PURE__ */ jsx7("div", { id: `help-center-drawer-${ticket.id}`, children: /* @__PURE__ */ jsx7(
1518
1535
  TicketDetailDrawer,
1519
1536
  {
1520
1537
  ticket,
@@ -1540,31 +1557,31 @@ function mapPriorityScheme(priority) {
1540
1557
 
1541
1558
  // src/components/tickets/help-center-create-form.tsx
1542
1559
  import { useState as useState5 } from "react";
1543
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1560
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1544
1561
  var SUBJECT_MAX_CHARS = 200;
1545
1562
  function HelpCenterCreateFormSkeleton() {
1546
- return /* @__PURE__ */ jsxs6("div", { className: "h-full flex flex-col border border-ods-border rounded-2xl md:rounded-3xl p-6 md:p-8 lg:p-10", children: [
1547
- /* @__PURE__ */ jsx7("div", { className: "mb-6 md:mb-8", children: /* @__PURE__ */ jsx7("div", { className: "h-10 w-72 bg-ods-border rounded animate-pulse mb-3 md:mb-4" }) }),
1548
- /* @__PURE__ */ jsxs6("div", { className: "flex flex-col flex-grow space-y-4 md:space-y-6", children: [
1549
- /* @__PURE__ */ jsx7("input", { type: "hidden", "aria-hidden": true }),
1550
- /* @__PURE__ */ jsx7("input", { type: "hidden", "aria-hidden": true }),
1551
- /* @__PURE__ */ jsx7("input", { type: "hidden", "aria-hidden": true }),
1552
- /* @__PURE__ */ jsx7("input", { type: "hidden", "aria-hidden": true }),
1553
- /* @__PURE__ */ jsxs6("div", { className: "flex flex-col", children: [
1554
- /* @__PURE__ */ jsx7("div", { className: "h-[27px] w-20 bg-ods-border rounded animate-pulse mb-1" }),
1555
- /* @__PURE__ */ jsx7("div", { className: "h-12 w-full bg-ods-border rounded animate-pulse" })
1563
+ return /* @__PURE__ */ jsxs7("div", { className: "h-full flex flex-col border border-ods-border rounded-2xl md:rounded-3xl p-6 md:p-8 lg:p-10", children: [
1564
+ /* @__PURE__ */ jsx8("div", { className: "mb-6 md:mb-8", children: /* @__PURE__ */ jsx8("div", { className: "h-10 w-72 bg-ods-border rounded animate-pulse mb-3 md:mb-4" }) }),
1565
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col flex-grow space-y-4 md:space-y-6", children: [
1566
+ /* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
1567
+ /* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
1568
+ /* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
1569
+ /* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
1570
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col", children: [
1571
+ /* @__PURE__ */ jsx8("div", { className: "h-[27px] w-20 bg-ods-border rounded animate-pulse mb-1" }),
1572
+ /* @__PURE__ */ jsx8("div", { className: "h-12 w-full bg-ods-border rounded animate-pulse" })
1556
1573
  ] }),
1557
- /* @__PURE__ */ jsxs6("div", { className: "flex flex-col flex-grow", children: [
1558
- /* @__PURE__ */ jsx7("div", { className: "h-[27px] w-32 bg-ods-border rounded animate-pulse mb-1" }),
1559
- /* @__PURE__ */ jsx7("div", { className: "h-24 w-full bg-ods-border rounded animate-pulse flex-grow" })
1574
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col flex-grow", children: [
1575
+ /* @__PURE__ */ jsx8("div", { className: "h-[27px] w-32 bg-ods-border rounded animate-pulse mb-1" }),
1576
+ /* @__PURE__ */ jsx8("div", { className: "h-24 w-full bg-ods-border rounded animate-pulse flex-grow" })
1560
1577
  ] }),
1561
- /* @__PURE__ */ jsx7("div", { className: "flex flex-col gap-2", children: /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
1562
- /* @__PURE__ */ jsx7("div", { className: "h-7 w-7 bg-ods-border rounded animate-pulse shrink-0" }),
1563
- /* @__PURE__ */ jsx7("div", { className: "h-4 w-40 bg-ods-border rounded animate-pulse" })
1578
+ /* @__PURE__ */ jsx8("div", { className: "flex flex-col gap-2", children: /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
1579
+ /* @__PURE__ */ jsx8("div", { className: "h-7 w-7 bg-ods-border rounded animate-pulse shrink-0" }),
1580
+ /* @__PURE__ */ jsx8("div", { className: "h-4 w-40 bg-ods-border rounded animate-pulse" })
1564
1581
  ] }) }),
1565
- /* @__PURE__ */ jsxs6("div", { className: "flex flex-col md:flex-row gap-4 md:gap-6 items-center justify-end w-full pt-2 mt-auto", children: [
1566
- /* @__PURE__ */ jsx7("div", { className: "h-4 w-72 bg-ods-border rounded animate-pulse" }),
1567
- /* @__PURE__ */ jsx7("div", { className: "h-12 w-32 bg-ods-border rounded animate-pulse" })
1582
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col md:flex-row gap-4 md:gap-6 items-center justify-end w-full pt-2 mt-auto", children: [
1583
+ /* @__PURE__ */ jsx8("div", { className: "h-4 w-72 bg-ods-border rounded animate-pulse" }),
1584
+ /* @__PURE__ */ jsx8("div", { className: "h-12 w-32 bg-ods-border rounded animate-pulse" })
1568
1585
  ] })
1569
1586
  ] })
1570
1587
  ] });
@@ -1577,12 +1594,12 @@ function HelpCenterCreateForm({
1577
1594
  }) {
1578
1595
  const [subject, setSubject] = useState5("");
1579
1596
  const [subjectError, setSubjectError] = useState5(null);
1580
- const subjectField = /* @__PURE__ */ jsxs6("div", { className: "flex flex-col", children: [
1581
- /* @__PURE__ */ jsxs6(Label, { htmlFor: "help-center-subject", children: [
1597
+ const subjectField = /* @__PURE__ */ jsxs7("div", { className: "flex flex-col", children: [
1598
+ /* @__PURE__ */ jsxs7(Label, { htmlFor: "help-center-subject", children: [
1582
1599
  "Subject",
1583
- /* @__PURE__ */ jsx7("span", { className: "text-ods-accent", children: "*" })
1600
+ /* @__PURE__ */ jsx8("span", { className: "text-ods-accent", children: "*" })
1584
1601
  ] }),
1585
- /* @__PURE__ */ jsx7(
1602
+ /* @__PURE__ */ jsx8(
1586
1603
  Input,
1587
1604
  {
1588
1605
  id: "help-center-subject",
@@ -1600,7 +1617,7 @@ function HelpCenterCreateForm({
1600
1617
  className: "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12"
1601
1618
  }
1602
1619
  ),
1603
- subjectError && /* @__PURE__ */ jsx7(
1620
+ subjectError && /* @__PURE__ */ jsx8(
1604
1621
  "span",
1605
1622
  {
1606
1623
  id: "help-center-subject-error",
@@ -1609,7 +1626,7 @@ function HelpCenterCreateForm({
1609
1626
  }
1610
1627
  )
1611
1628
  ] });
1612
- return /* @__PURE__ */ jsx7(
1629
+ return /* @__PURE__ */ jsx8(
1613
1630
  ContactForm,
1614
1631
  {
1615
1632
  title: "Open a new ticket",
@@ -1646,7 +1663,7 @@ function HelpCenterCreateForm({
1646
1663
  }
1647
1664
 
1648
1665
  // src/components/tickets/help-center-list.tsx
1649
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1666
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1650
1667
  function HelpCenterList({ toast: toast2 = toast } = {}) {
1651
1668
  const identity = useChatIdentity();
1652
1669
  const searchParams = useSearchParams();
@@ -1654,13 +1671,14 @@ function HelpCenterList({ toast: toast2 = toast } = {}) {
1654
1671
  const pathname = usePathname();
1655
1672
  const search = searchParams.get("search") || "";
1656
1673
  const status = searchParams.get("status") || "all";
1674
+ const ticketParam = searchParams.get("ticket") || "";
1657
1675
  const rawPage = Number(searchParams.get("page"));
1658
1676
  const page = Number.isFinite(rawPage) && rawPage > 0 ? Math.floor(rawPage) : 1;
1659
1677
  if (identity.isLoading) {
1660
- return /* @__PURE__ */ jsx8(DevSectionPage, { sectionKey: "tickets", preControls: /* @__PURE__ */ jsx8(HelpCenterCreateFormSkeleton, {}), children: /* @__PURE__ */ jsx8(DevCardRowSkeletonList, {}) });
1678
+ return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", preControls: /* @__PURE__ */ jsx9(HelpCenterCreateFormSkeleton, {}), children: /* @__PURE__ */ jsx9(DevCardRowSkeletonList, {}) });
1661
1679
  }
1662
1680
  if (identity.authTier === "anon" || !identity.user?.email) {
1663
- return /* @__PURE__ */ jsx8(DevSectionPage, { sectionKey: "tickets", children: /* @__PURE__ */ jsx8(
1681
+ return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", children: /* @__PURE__ */ jsx9(
1664
1682
  EmptyState,
1665
1683
  {
1666
1684
  type: "generic",
@@ -1672,12 +1690,13 @@ function HelpCenterList({ toast: toast2 = toast } = {}) {
1672
1690
  }
1673
1691
  const sessionName = [identity.user?.firstName, identity.user?.lastName].filter(Boolean).join(" ").trim() || identity.user?.email?.split("@")[0] || "Customer";
1674
1692
  const sessionEmail = identity.user.email;
1675
- return /* @__PURE__ */ jsx8(
1693
+ return /* @__PURE__ */ jsx9(
1676
1694
  HelpCenterListAuthed,
1677
1695
  {
1678
1696
  search,
1679
1697
  status,
1680
1698
  page,
1699
+ ticketParam,
1681
1700
  searchParams,
1682
1701
  router,
1683
1702
  pathname,
@@ -1691,6 +1710,7 @@ function HelpCenterListAuthed({
1691
1710
  search,
1692
1711
  status,
1693
1712
  page,
1713
+ ticketParam,
1694
1714
  searchParams,
1695
1715
  router,
1696
1716
  pathname,
@@ -1699,6 +1719,18 @@ function HelpCenterListAuthed({
1699
1719
  sessionEmail
1700
1720
  }) {
1701
1721
  const queryClient = useQueryClient3();
1722
+ const [optimisticTickets, setOptimisticTickets] = useState6([]);
1723
+ const [supportSystemDown, setSupportSystemDown] = useState6(false);
1724
+ const setOpenTicket = useCallback6(
1725
+ (externalId) => {
1726
+ const params = new URLSearchParams(searchParams.toString());
1727
+ if (externalId) params.set("ticket", externalId);
1728
+ else params.delete("ticket");
1729
+ const qs = params.toString();
1730
+ router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
1731
+ },
1732
+ [searchParams, router, pathname]
1733
+ );
1702
1734
  const { tickets, isLoading, isFetching, error, refetch, totalPages } = useTicketsList({
1703
1735
  // `sessionEmail` is drilled in from the parent — see the same
1704
1736
  // pattern + race-cause rationale documented in
@@ -1709,19 +1741,21 @@ function HelpCenterListAuthed({
1709
1741
  customerEmail: sessionEmail,
1710
1742
  search,
1711
1743
  status,
1712
- page
1744
+ page,
1745
+ // Live status: while a drawer is open, poll so an out-of-band HubSpot
1746
+ // status change (e.g. agent closes the ticket) flips the badge +
1747
+ // open/reopen affordance within one interval. Idle (no drawer) → no poll.
1748
+ // `ticketParam` (the open ticket's external_id) is the open signal.
1749
+ refetchInterval: ticketParam ? TICKET_LIVE_POLL_MS : false
1713
1750
  });
1714
- const [optimisticTickets, setOptimisticTickets] = useState6([]);
1715
- const [expandedTicketId, setExpandedTicketId] = useState6(null);
1716
- const [supportSystemDown, setSupportSystemDown] = useState6(false);
1717
- const prependOptimistic = useCallback5((placeholder) => {
1751
+ const expandedTicketId = ticketParam && tickets.find((t) => t.external_id === ticketParam)?.id || null;
1752
+ const prependOptimistic = useCallback6((placeholder) => {
1718
1753
  setOptimisticTickets((prev) => [placeholder, ...prev]);
1719
1754
  }, []);
1720
- const removeOptimistic = useCallback5((placeholderId) => {
1755
+ const removeOptimistic = useCallback6((placeholderId) => {
1721
1756
  setOptimisticTickets((prev) => prev.filter((t) => t.id !== placeholderId));
1722
- setExpandedTicketId((prev) => prev === placeholderId ? null : prev);
1723
1757
  }, []);
1724
- const removeTicketFromCache = useCallback5(
1758
+ const removeTicketFromCache = useCallback6(
1725
1759
  (ticketId) => {
1726
1760
  queryClient.setQueriesData(
1727
1761
  { queryKey: ["tickets"] },
@@ -1732,7 +1766,6 @@ function HelpCenterListAuthed({
1732
1766
  return { ...prev, tickets: nextTickets };
1733
1767
  }
1734
1768
  );
1735
- setExpandedTicketId((prev) => prev === ticketId ? null : prev);
1736
1769
  },
1737
1770
  [queryClient]
1738
1771
  );
@@ -1743,13 +1776,18 @@ function HelpCenterListAuthed({
1743
1776
  toast: toast2,
1744
1777
  onSupportSystemDown: () => setSupportSystemDown(true)
1745
1778
  });
1746
- const toggleRow = useCallback5((id) => {
1747
- setExpandedTicketId((prev) => prev === id ? null : id);
1748
- }, []);
1779
+ const toggleRow = useCallback6(
1780
+ (id) => {
1781
+ const t = tickets.find((x) => x.id === id);
1782
+ if (!t?.external_id) return;
1783
+ setOpenTicket(t.external_id === ticketParam ? null : t.external_id);
1784
+ },
1785
+ [tickets, ticketParam, setOpenTicket]
1786
+ );
1749
1787
  const merged = [...optimisticTickets, ...tickets];
1750
1788
  const hasActiveFilters = search !== "" || status !== "" && status !== "all";
1751
1789
  const hasResults = merged.length > 0;
1752
- const form = /* @__PURE__ */ jsx8(
1790
+ const form = /* @__PURE__ */ jsx9(
1753
1791
  HelpCenterCreateForm,
1754
1792
  {
1755
1793
  actions,
@@ -1758,22 +1796,22 @@ function HelpCenterListAuthed({
1758
1796
  supportSystemDown
1759
1797
  }
1760
1798
  );
1761
- const body = /* @__PURE__ */ jsxs7("div", { className: "w-full flex flex-col gap-[40px]", children: [
1762
- error && /* @__PURE__ */ jsxs7("div", { className: "bg-ods-card border border-ods-border rounded-[6px] p-[40px] text-center w-full flex flex-col items-center gap-3", children: [
1763
- /* @__PURE__ */ jsxs7("p", { className: "text-ods-error text-base", children: [
1799
+ const body = /* @__PURE__ */ jsxs8("div", { className: "w-full flex flex-col gap-[40px]", children: [
1800
+ error && /* @__PURE__ */ jsxs8("div", { className: "bg-ods-card border border-ods-border rounded-[6px] p-[40px] text-center w-full flex flex-col items-center gap-3", children: [
1801
+ /* @__PURE__ */ jsxs8("p", { className: "text-ods-error text-base", children: [
1764
1802
  "Couldn\u2019t load your tickets. ",
1765
1803
  error.message
1766
1804
  ] }),
1767
- /* @__PURE__ */ jsx8(Button, { type: "button", variant: "accent", onClick: () => refetch(), children: "Retry" })
1805
+ /* @__PURE__ */ jsx9(Button, { type: "button", variant: "accent", onClick: () => refetch(), children: "Retry" })
1768
1806
  ] }),
1769
- !error && /* @__PURE__ */ jsx8("div", { className: "w-full", children: isLoading ? /* @__PURE__ */ jsx8(DevCardRowSkeletonList, {}) : !hasResults && isFetching ? (
1807
+ !error && /* @__PURE__ */ jsx9("div", { className: "w-full", children: isLoading ? /* @__PURE__ */ jsx9(DevCardRowSkeletonList, {}) : !hasResults && isFetching ? (
1770
1808
  // Bridge state — background refetch in flight and the
1771
1809
  // optimistic placeholder was just removed by the mutation
1772
1810
  // callback. Without this branch "No tickets yet" would flash
1773
1811
  // for ~50ms between `removeOptimistic` and the server
1774
1812
  // response landing.
1775
- /* @__PURE__ */ jsx8(DevCardRowSkeletonList, { rows: 1 })
1776
- ) : !hasResults ? hasActiveFilters ? /* @__PURE__ */ jsx8(
1813
+ /* @__PURE__ */ jsx9(DevCardRowSkeletonList, { rows: 1 })
1814
+ ) : !hasResults ? hasActiveFilters ? /* @__PURE__ */ jsx9(
1777
1815
  EmptyState,
1778
1816
  {
1779
1817
  type: "search",
@@ -1788,7 +1826,7 @@ function HelpCenterListAuthed({
1788
1826
  router.replace(`${pathname}?${params.toString()}`, { scroll: false });
1789
1827
  }
1790
1828
  }
1791
- ) : /* @__PURE__ */ jsx8(
1829
+ ) : /* @__PURE__ */ jsx9(
1792
1830
  EmptyState,
1793
1831
  {
1794
1832
  type: "generic",
@@ -1805,7 +1843,7 @@ function HelpCenterListAuthed({
1805
1843
  // hidden) instead of bubbling up to the window. `clip`
1806
1844
  // keeps the visual clip but NOT the scroll-container
1807
1845
  // status, so click-to-scroll actually moves the page.
1808
- /* @__PURE__ */ jsx8("div", { className: "bg-ods-card border border-ods-border rounded-[6px] overflow-clip w-full", children: merged.map((ticket) => /* @__PURE__ */ jsx8(
1846
+ /* @__PURE__ */ jsx9("div", { className: "bg-ods-card border border-ods-border rounded-[6px] overflow-clip w-full", children: merged.map((ticket) => /* @__PURE__ */ jsx9(
1809
1847
  HelpCenterCard,
1810
1848
  {
1811
1849
  ticket,
@@ -1816,16 +1854,16 @@ function HelpCenterListAuthed({
1816
1854
  onSendMessage: actions.sendMessage,
1817
1855
  onClose: actions.closeTicket,
1818
1856
  onReopen: actions.reopenTicket,
1819
- onActionCollapsed: () => setExpandedTicketId(null),
1857
+ onActionCollapsed: () => setOpenTicket(null),
1820
1858
  replyError: actions.replyErrorFor(ticket.external_id),
1821
1859
  onClearReplyError: () => actions.clearReplyError(ticket.external_id)
1822
1860
  },
1823
1861
  ticket.id
1824
1862
  )) })
1825
1863
  ) }),
1826
- !error && totalPages > 1 && /* @__PURE__ */ jsx8(UnifiedPagination, { currentPage: page, totalPages })
1864
+ !error && totalPages > 1 && /* @__PURE__ */ jsx9(UnifiedPagination, { currentPage: page, totalPages })
1827
1865
  ] });
1828
- return /* @__PURE__ */ jsx8(DevSectionPage, { sectionKey: "tickets", preControls: form, children: body });
1866
+ return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", preControls: form, children: body });
1829
1867
  }
1830
1868
  export {
1831
1869
  HelpCenterCard,
@@ -1837,6 +1875,7 @@ export {
1837
1875
  TicketDetailDrawer,
1838
1876
  TicketLinkedDeliveryCard,
1839
1877
  TicketOpenForm,
1878
+ TicketReplyComposer,
1840
1879
  TicketRow,
1841
1880
  isOptimistic,
1842
1881
  mapTicketActionError,