@flamingo-stack/openframe-frontend-core 0.0.216 → 0.0.217-snapshot.20260601003634

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 +309 -256
  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 +376 -323
  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 +41 -5
  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,15 +229,16 @@ 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
- const queryEnabled = enabled && identity.authTier !== "anon" && !!identity.user?.email && !!externalTicketId && !externalTicketId.startsWith("temp-");
240
+ const fetchable = enabled && !!externalTicketId && !externalTicketId.startsWith("temp-");
241
+ const queryEnabled = fetchable && identity.authTier !== "anon" && !!identity.user?.email;
238
242
  const query = useQuery({
239
243
  queryKey: ["ticket-engagements", externalTicketId, identityKey],
240
244
  enabled: queryEnabled,
@@ -245,6 +249,11 @@ function useTicketEngagements(externalTicketId, enabled = true) {
245
249
  gcTime: 0,
246
250
  refetchOnMount: "always",
247
251
  refetchOnWindowFocus: true,
252
+ // Live conversation: poll while the caller opts in (drawer open). New
253
+ // agent replies + attachments appear within one interval without a
254
+ // manual refresh. `refetchIntervalInBackground` stays false (default)
255
+ // so polling pauses on a hidden tab.
256
+ refetchInterval,
248
257
  queryFn: async () => {
249
258
  const response = await embedAuthedFetch(LIST_ENGAGEMENTS_ENDPOINT, {
250
259
  method: "POST",
@@ -260,7 +269,20 @@ function useTicketEngagements(externalTicketId, enabled = true) {
260
269
  });
261
270
  return {
262
271
  engagements: query.data ?? [],
263
- isLoading: queryEnabled && query.isLoading,
272
+ // Loading-state truth that prevents the "body → blink → skeleton → data"
273
+ // double-flash. The bug: `useChatIdentity` starts at anon defaults and
274
+ // resolves async, so on the first render `queryEnabled` is false and the
275
+ // OLD `queryEnabled && query.isLoading` returned FALSE — the panel rendered
276
+ // the ticket body, THEN identity resolved, the query enabled, isLoading
277
+ // flipped true → skeleton appeared (the blink), then data landed.
278
+ //
279
+ // Fix: for a fetchable ticket we are "loading" whenever we don't yet have
280
+ // the timeline to show — that includes the window while identity is still
281
+ // resolving (so we skeleton from the FIRST render, never the body) AND the
282
+ // cold query fetch (`data === undefined`). A background poll keeps
283
+ // `query.data` defined, so it never re-flashes the skeleton. Non-fetchable
284
+ // (optimistic/disabled) or a resolved-anon viewer → not loading.
285
+ isLoading: fetchable && (identity.isLoading || queryEnabled && query.data === void 0),
264
286
  isFetching: query.isFetching,
265
287
  error: query.error ?? null,
266
288
  refetch: () => {
@@ -305,8 +327,134 @@ function TicketLinkedDeliveryCard({
305
327
  );
306
328
  }
307
329
 
308
- // src/components/tickets/ticket-detail-drawer.tsx
330
+ // src/components/tickets/ticket-reply-composer.tsx
331
+ init_button();
332
+ import { useCallback, useState as useState2 } from "react";
309
333
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
334
+ function TicketReplyComposer({
335
+ ticket,
336
+ busy,
337
+ supportSystemDown,
338
+ onSendMessage,
339
+ onClose
340
+ }) {
341
+ const [resolution, setResolution] = useState2("");
342
+ const [closeDialogOpen, setCloseDialogOpen] = useState2(false);
343
+ const attachments = useChatAttachments();
344
+ const ticketRef = { id: ticket.id, external_id: ticket.external_id };
345
+ const hasReadyFiles = attachments.readyAttachments.length > 0;
346
+ const handleSend = useCallback(
347
+ async (text) => {
348
+ const ref = { id: ticket.id, external_id: ticket.external_id };
349
+ const ok = await onSendMessage(ref, text.trim(), attachments.readyAttachments);
350
+ if (ok) attachments.clear();
351
+ return ok;
352
+ },
353
+ // Depend on the reactive projections, not the whole bag (a fresh object each
354
+ // render). `readyAttachments` is memo-stable; `clear` is callback-stable.
355
+ [
356
+ onSendMessage,
357
+ ticket.id,
358
+ ticket.external_id,
359
+ attachments.readyAttachments,
360
+ attachments.clear
361
+ ]
362
+ );
363
+ const confirmClose = async () => {
364
+ setCloseDialogOpen(false);
365
+ await onClose(ticketRef, resolution.trim() || void 0);
366
+ setResolution("");
367
+ };
368
+ const disabled = busy || supportSystemDown;
369
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2", children: [
370
+ /* @__PURE__ */ jsx3(
371
+ ChatAttachmentChipStrip,
372
+ {
373
+ attachments: attachments.attachments,
374
+ onRemove: attachments.removeAttachment,
375
+ disabled,
376
+ size: "compact"
377
+ }
378
+ ),
379
+ /* @__PURE__ */ jsx3(
380
+ ChatInput,
381
+ {
382
+ fullWidth: true,
383
+ autoFocus: true,
384
+ placeholder: "Type a reply\u2026",
385
+ sending: busy || attachments.hasInflightUploads,
386
+ disabled: supportSystemDown,
387
+ allowEmptySend: hasReadyFiles,
388
+ maxLength: TICKET_TEXT_MAX_CHARS,
389
+ onSend: handleSend
390
+ }
391
+ ),
392
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 w-full", children: [
393
+ !supportSystemDown && /* @__PURE__ */ jsx3(
394
+ ChatAttachmentAddButton,
395
+ {
396
+ attachmentsEnabled: true,
397
+ attachmentsCount: attachments.attachments.length,
398
+ onAddFiles: attachments.addFiles,
399
+ disabled,
400
+ size: "compact"
401
+ }
402
+ ),
403
+ /* @__PURE__ */ jsx3("div", { className: "flex-1 min-w-0" }),
404
+ /* @__PURE__ */ jsx3(
405
+ Button,
406
+ {
407
+ type: "button",
408
+ variant: "transparent",
409
+ size: "small",
410
+ onClick: () => setCloseDialogOpen(true),
411
+ disabled,
412
+ className: "text-ods-text-secondary hover:text-ods-text-primary",
413
+ children: "Close ticket"
414
+ }
415
+ )
416
+ ] }),
417
+ /* @__PURE__ */ jsx3(AlertDialog, { open: closeDialogOpen, onOpenChange: setCloseDialogOpen, children: /* @__PURE__ */ jsxs2(AlertDialogContent, { className: "bg-ods-card border-ods-border", children: [
418
+ /* @__PURE__ */ jsxs2(AlertDialogHeader, { children: [
419
+ /* @__PURE__ */ jsx3(AlertDialogTitle, { className: "text-ods-text-primary", children: "Close this ticket?" }),
420
+ /* @__PURE__ */ jsx3(AlertDialogDescription, { className: "text-ods-text-secondary", children: "Add an optional resolution note below. You can reopen the ticket later if needed." })
421
+ ] }),
422
+ /* @__PURE__ */ jsx3(
423
+ Textarea,
424
+ {
425
+ value: resolution,
426
+ onChange: (e) => setResolution(e.target.value),
427
+ placeholder: "Resolution (optional)",
428
+ rows: 3,
429
+ maxLength: TICKET_TEXT_MAX_CHARS,
430
+ className: "mt-2"
431
+ }
432
+ ),
433
+ /* @__PURE__ */ jsxs2(AlertDialogFooter, { children: [
434
+ /* @__PURE__ */ jsx3(
435
+ AlertDialogCancel,
436
+ {
437
+ disabled: busy,
438
+ className: "bg-transparent border-ods-border text-ods-text-primary hover:bg-ods-border",
439
+ children: "Cancel"
440
+ }
441
+ ),
442
+ /* @__PURE__ */ jsx3(
443
+ AlertDialogAction,
444
+ {
445
+ onClick: () => void confirmClose(),
446
+ disabled: busy,
447
+ className: "bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover",
448
+ children: "Close ticket"
449
+ }
450
+ )
451
+ ] })
452
+ ] }) })
453
+ ] });
454
+ }
455
+
456
+ // src/components/tickets/ticket-detail-drawer.tsx
457
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
310
458
  function TicketDetailDrawer({
311
459
  ticket,
312
460
  busy,
@@ -319,22 +467,22 @@ function TicketDetailDrawer({
319
467
  onClearReplyError
320
468
  }) {
321
469
  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 })
470
+ return /* @__PURE__ */ jsxs3("div", { className: "bg-ods-card border-t border-ods-border px-4 py-4 flex flex-col gap-4", children: [
471
+ /* @__PURE__ */ jsx4(AssignedAgentRow, { assignedOwner: ticket.assignedOwner }),
472
+ ticket.clickup && /* @__PURE__ */ jsx4(TicketLinkedDeliveryCard, { clickup: ticket.clickup }),
473
+ /* @__PURE__ */ jsxs3("div", { children: [
474
+ /* @__PURE__ */ jsx4("p", { className: "text-xs font-medium text-ods-text-secondary mb-2 uppercase tracking-wider", children: "Conversation" }),
475
+ /* @__PURE__ */ jsx4(TicketTimelinePanel, { ticket })
328
476
  ] }),
329
- /* @__PURE__ */ jsxs2("div", { className: "border-t border-ods-border pt-4", children: [
330
- replyError && /* @__PURE__ */ jsx3(
477
+ /* @__PURE__ */ jsxs3("div", { className: "border-t border-ods-border pt-4", children: [
478
+ replyError && /* @__PURE__ */ jsx4(
331
479
  ReplyFailureBanner,
332
480
  {
333
481
  error: replyError,
334
482
  onDismiss: onClearReplyError ?? (() => void 0)
335
483
  }
336
484
  ),
337
- isClosed ? /* @__PURE__ */ jsx3(
485
+ isClosed ? /* @__PURE__ */ jsx4(
338
486
  ReopenAction,
339
487
  {
340
488
  ticketRef: { id: ticket.id, external_id: ticket.external_id },
@@ -343,37 +491,49 @@ function TicketDetailDrawer({
343
491
  onReopen,
344
492
  onActionCollapsed
345
493
  }
346
- ) : /* @__PURE__ */ jsx3(
347
- OpenActions,
494
+ ) : /* @__PURE__ */ jsx4(
495
+ TicketReplyComposer,
348
496
  {
349
497
  ticket,
350
498
  busy,
351
499
  supportSystemDown,
352
500
  onSendMessage,
353
- onClose,
354
- onActionCollapsed
501
+ onClose
355
502
  }
356
503
  )
357
504
  ] })
358
505
  ] });
359
506
  }
360
507
  var TURN_SEPARATOR_RE = /[\s]{1,16}---[\s]{1,16}/g;
508
+ var TICKET_FEED_FRAME = "bg-ods-card border border-ods-border rounded-[6px] overflow-y-auto w-full";
509
+ var TICKET_FEED_HEIGHT = "h-[60vh] md:h-[420px]";
510
+ var TICKET_FEED_INNER = "flex flex-col gap-4 md:gap-6 px-4 md:px-6 py-4 md:py-6";
511
+ var TICKET_FEED_SKELETON_ROWS = 6;
361
512
  function TicketTimelinePanel({ ticket }) {
362
513
  const identity = useChatIdentity();
363
514
  const externalId = isOptimistic(ticket) ? null : ticket.external_id;
364
- const { engagements, isLoading } = useTicketEngagements(externalId, !!externalId);
515
+ const { engagements, isLoading } = useTicketEngagements(
516
+ externalId,
517
+ !!externalId,
518
+ TICKET_LIVE_POLL_MS
519
+ );
520
+ const { scrollRef, contentRef } = useStickToBottom({ initial: "instant", resize: "smooth" });
365
521
  const bodyTurns = ticket.body ? ticket.body.split(TURN_SEPARATOR_RE).map((t) => t.trim()).filter(Boolean) : [];
522
+ const customerEngagementBodies = new Set(
523
+ engagements.filter((e) => e.authorRole === "customer").map((e) => (e.body ?? "").trim()).filter(Boolean)
524
+ );
525
+ const suppressBodyTurnZero = bodyTurns.length > 0 && customerEngagementBodies.has(bodyTurns[0]);
366
526
  const sessionEmailLower = identity.user?.email?.trim().toLowerCase() ?? null;
367
527
  const isViewerTheCustomer = !!sessionEmailLower && ticket.customer_emails.some((e) => e.trim().toLowerCase() === sessionEmailLower);
368
528
  const viewerName = identity.user?.name?.trim() || null;
369
529
  const ticketCustomerName = ticket.customer_name?.trim() || null;
370
530
  const customerName = (isViewerTheCustomer ? viewerName : null) || ticketCustomerName || viewerName || identity.user?.email || "You";
371
531
  const customerAvatar = isViewerTheCustomer ? identity.user?.avatarUrl ?? void 0 : void 0;
532
+ if (isLoading) {
533
+ 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)) }) });
534
+ }
372
535
  if (bodyTurns.length === 0 && engagements.length === 0) {
373
- if (isLoading) {
374
- return /* @__PURE__ */ jsx3(ConversationCardRowSkeletonList, { rows: 2 });
375
- }
376
- return /* @__PURE__ */ jsx3(
536
+ return /* @__PURE__ */ jsx4(
377
537
  EmptyState,
378
538
  {
379
539
  type: "generic",
@@ -383,19 +543,17 @@ function TicketTimelinePanel({ ticket }) {
383
543
  }
384
544
  );
385
545
  }
386
- return /* @__PURE__ */ jsxs2("div", { className: "bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full", children: [
546
+ 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
547
  bodyTurns.map((turn, i) => {
548
+ if (i === 0 && suppressBodyTurnZero) return null;
388
549
  const isResolution = turn.startsWith("[Resolution]");
389
- const role = i === 0 ? "Original message" : isResolution ? "Resolution" : `Update ${i}`;
390
550
  const text = isResolution ? turn.replace(/^\[Resolution\]\s*/, "") : turn;
391
- return /* @__PURE__ */ jsx3(
392
- ConversationCardRow,
551
+ return /* @__PURE__ */ jsx4(
552
+ ChatMessageRow,
393
553
  {
394
- author: customerName,
395
- role,
396
- avatarSrc: customerAvatar,
397
- body: text,
398
- variant: "current-user"
554
+ displayName: customerName,
555
+ avatarUrl: customerAvatar,
556
+ body: text
399
557
  },
400
558
  `body-${i}-${turn.slice(0, 24)}`
401
559
  );
@@ -418,32 +576,30 @@ function TicketTimelinePanel({ ticket }) {
418
576
  author = "Support team";
419
577
  avatarSrc = void 0;
420
578
  }
421
- return /* @__PURE__ */ jsx3(
422
- ConversationCardRow,
579
+ const engAttachments = mapEngagementAttachments(eng.attachments);
580
+ return /* @__PURE__ */ jsx4(
581
+ ChatMessageRow,
423
582
  {
424
- author,
425
- role: "Reply",
426
- avatarSrc,
427
- timestamp: eng.createdAt,
583
+ displayName: author,
584
+ avatarUrl: avatarSrc,
585
+ timeLabel: eng.createdAt ? formatRelativeTime(eng.createdAt) : null,
428
586
  body: stripAttachmentsPreamble(eng.body ?? ""),
429
- attachments: mapEngagementAttachments(eng.attachments),
430
- variant: isCustomer ? "current-user" : "support"
587
+ footer: engAttachments.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "mt-2", children: /* @__PURE__ */ jsx4(TicketAttachmentsList, { attachments: engAttachments, size: "compact" }) }) : null
431
588
  },
432
589
  eng.id
433
590
  );
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
- ] });
591
+ })
592
+ ] }) });
441
593
  }
442
594
  function mapEngagementAttachments(files) {
443
595
  return files.map((f) => ({
444
596
  id: f.id,
445
597
  fileName: f.name ?? `file-${f.id}`,
446
598
  fileSize: f.size ? formatBytes(f.size) : "",
599
+ // Show an inline thumbnail for image attachments (the signed `url` is a
600
+ // viewable URL). Non-images fall back to the file-type icon. SquareAvatar
601
+ // degrades to initials on a broken/expired image URL.
602
+ thumbnailSrc: f.url && (f.mime?.startsWith("image/") ?? false) ? f.url : void 0,
447
603
  onDownload: f.url ? () => window.open(f.url, "_blank", "noopener,noreferrer") : void 0
448
604
  }));
449
605
  }
@@ -466,10 +622,12 @@ function ReopenAction({
466
622
  const handleReopen = async () => {
467
623
  void await onReopen(ticketRef);
468
624
  };
469
- return /* @__PURE__ */ jsx3("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx3(
625
+ return /* @__PURE__ */ jsx4("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx4(
470
626
  Button,
471
627
  {
472
628
  type: "button",
629
+ variant: "outline",
630
+ size: "small",
473
631
  onClick: () => void handleReopen(),
474
632
  disabled: busy || supportSystemDown,
475
633
  loading: busy,
@@ -477,146 +635,19 @@ function ReopenAction({
477
635
  }
478
636
  ) });
479
637
  }
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
638
  function ReplyFailureBanner({
608
639
  error,
609
640
  onDismiss
610
641
  }) {
611
- return /* @__PURE__ */ jsxs2(
642
+ return /* @__PURE__ */ jsxs3(
612
643
  "div",
613
644
  {
614
645
  role: "status",
615
646
  "aria-live": "polite",
616
647
  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
648
  children: [
618
- /* @__PURE__ */ jsx3("span", { className: "font-medium leading-snug", children: error.message }),
619
- /* @__PURE__ */ jsx3(
649
+ /* @__PURE__ */ jsx4("span", { className: "font-medium leading-snug", children: error.message }),
650
+ /* @__PURE__ */ jsx4(
620
651
  Button,
621
652
  {
622
653
  type: "button",
@@ -637,10 +668,10 @@ function AssignedAgentRow({
637
668
  const trimmedName = assignedOwner?.name?.trim() || null;
638
669
  const emailFallback = assignedOwner?.email?.trim() || null;
639
670
  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(
671
+ return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-xs", children: [
672
+ /* @__PURE__ */ jsx4("span", { className: "text-ods-text-secondary uppercase tracking-wider font-medium", children: "Assigned to" }),
673
+ displayLabel ? /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-1.5 text-ods-text-primary font-medium", children: [
674
+ /* @__PURE__ */ jsx4(
644
675
  SquareAvatar,
645
676
  {
646
677
  size: "sm",
@@ -651,12 +682,12 @@ function AssignedAgentRow({
651
682
  }
652
683
  ),
653
684
  displayLabel
654
- ] }) : /* @__PURE__ */ jsx3("span", { className: "text-ods-text-secondary italic", children: "Unassigned" })
685
+ ] }) : /* @__PURE__ */ jsx4("span", { className: "text-ods-text-secondary italic", children: "Unassigned" })
655
686
  ] });
656
687
  }
657
688
 
658
689
  // src/components/tickets/ticket-row.tsx
659
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
690
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
660
691
  function TicketRow({
661
692
  ticket,
662
693
  expanded,
@@ -670,7 +701,7 @@ function TicketRow({
670
701
  }) {
671
702
  const optimistic = isOptimistic(ticket);
672
703
  const rowRef = useRef(null);
673
- const handleClick = useCallback(() => {
704
+ const handleClick = useCallback2(() => {
674
705
  onToggle(ticket.id);
675
706
  scrollElementIntoView(rowRef.current, {
676
707
  adjustTargetY: (raw) => {
@@ -706,13 +737,13 @@ function TicketRow({
706
737
  // when the task exists but its status hasn't synced yet.
707
738
  linkedTaskLabel: ticket.clickup ? ticket.clickup.status ? ticket.clickup.status.replace(/\b\w/g, (c) => c.toUpperCase()) : "Linked work" : void 0
708
739
  };
709
- return /* @__PURE__ */ jsx4("div", { ref: rowRef, className: "scroll-mt-24", children: /* @__PURE__ */ jsxs3(
740
+ return /* @__PURE__ */ jsx5("div", { ref: rowRef, className: "scroll-mt-24", children: /* @__PURE__ */ jsxs4(
710
741
  Collapsible,
711
742
  {
712
743
  open: expanded && !optimistic,
713
744
  className: "border-b border-ods-border last:border-b-0",
714
745
  children: [
715
- /* @__PURE__ */ jsx4(
746
+ /* @__PURE__ */ jsx5(
716
747
  ChatTicketItem,
717
748
  {
718
749
  ticket: tileData,
@@ -721,12 +752,12 @@ function TicketRow({
721
752
  "aria-controls": `ticket-drawer-${ticket.id}`
722
753
  }
723
754
  ),
724
- /* @__PURE__ */ jsx4(
755
+ /* @__PURE__ */ jsx5(
725
756
  CollapsibleContent2,
726
757
  {
727
758
  id: `ticket-drawer-${ticket.id}`,
728
759
  className: "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
729
- children: /* @__PURE__ */ jsx4(
760
+ children: /* @__PURE__ */ jsx5(
730
761
  TicketDetailDrawer,
731
762
  {
732
763
  ticket,
@@ -758,6 +789,7 @@ function useTicketsList(filters) {
758
789
  const pageSize = Math.max(1, Math.min(100, Math.floor(filters.pageSize ?? DEFAULT_PAGE_SIZE) || DEFAULT_PAGE_SIZE));
759
790
  const enabled = !!customerEmail;
760
791
  const identityKey = customerEmail || "anon";
792
+ const refetchInterval = filters.refetchInterval ?? false;
761
793
  const query = useQuery2({
762
794
  queryKey: ["tickets", "self", identityKey, search, statusFilter, page, pageSize],
763
795
  enabled,
@@ -770,6 +802,10 @@ function useTicketsList(filters) {
770
802
  gcTime: 0,
771
803
  refetchOnMount: "always",
772
804
  refetchOnWindowFocus: true,
805
+ // Live status: poll while the caller opts in (drawer open). Defaults to
806
+ // false. `refetchIntervalInBackground` stays false (the default) so polling
807
+ // pauses on a hidden tab — no wasted requests when the user tabs away.
808
+ refetchInterval,
773
809
  queryFn: async () => {
774
810
  const body = {
775
811
  query: search,
@@ -831,7 +867,7 @@ function useTicketsList(filters) {
831
867
  }
832
868
 
833
869
  // src/components/tickets/hooks/use-ticket-actions.ts
834
- import { useCallback as useCallback2, useEffect, useMemo, useRef as useRef2, useState as useState3 } from "react";
870
+ import { useCallback as useCallback3, useEffect, useMemo, useRef as useRef2, useState as useState3 } from "react";
835
871
  import { useQueryClient } from "@tanstack/react-query";
836
872
  var TICKET_ACTION_ENDPOINT = "/api/chat/agent/ticket-action";
837
873
  var REPLY_BANNER_CODES = /* @__PURE__ */ new Set([
@@ -848,14 +884,14 @@ function useTicketActions(options) {
848
884
  const [isSubmittingForm, setIsSubmittingForm] = useState3(false);
849
885
  const busyRowsRef = useRef2(/* @__PURE__ */ new Set());
850
886
  const [busyRows, setBusyRows] = useState3(() => /* @__PURE__ */ new Set());
851
- const setRowBusy = useCallback2((id, busy) => {
887
+ const setRowBusy = useCallback3((id, busy) => {
852
888
  if (busy) busyRowsRef.current.add(id);
853
889
  else busyRowsRef.current.delete(id);
854
890
  setBusyRows(new Set(busyRowsRef.current));
855
891
  }, []);
856
- const isRowBusy = useCallback2((id) => busyRows.has(id), [busyRows]);
892
+ const isRowBusy = useCallback3((id) => busyRows.has(id), [busyRows]);
857
893
  const [replyErrorByTicket, setReplyErrorByTicket] = useState3(() => /* @__PURE__ */ new Map());
858
- const setReplyError = useCallback2(
894
+ const setReplyError = useCallback3(
859
895
  (externalId, mapped) => {
860
896
  setReplyErrorByTicket((prev) => {
861
897
  const next = new Map(prev);
@@ -866,11 +902,11 @@ function useTicketActions(options) {
866
902
  },
867
903
  []
868
904
  );
869
- const replyErrorFor = useCallback2(
905
+ const replyErrorFor = useCallback3(
870
906
  (externalId) => replyErrorByTicket.get(externalId) ?? null,
871
907
  [replyErrorByTicket]
872
908
  );
873
- const clearReplyError = useCallback2(
909
+ const clearReplyError = useCallback3(
874
910
  (externalId) => setReplyError(externalId, null),
875
911
  [setReplyError]
876
912
  );
@@ -884,12 +920,12 @@ function useTicketActions(options) {
884
920
  };
885
921
  }, []);
886
922
  const queueRef = useRef2(Promise.resolve());
887
- const enqueue = useCallback2((work) => {
923
+ const enqueue = useCallback3((work) => {
888
924
  const next = queueRef.current.then(work, work);
889
925
  queueRef.current = next.catch(() => void 0);
890
926
  return next;
891
927
  }, []);
892
- const executeTicketAction = useCallback2(
928
+ const executeTicketAction = useCallback3(
893
929
  async (toolName, args) => {
894
930
  const res = await embedAuthedFetch(TICKET_ACTION_ENDPOINT, {
895
931
  method: "POST",
@@ -905,7 +941,7 @@ function useTicketActions(options) {
905
941
  },
906
942
  []
907
943
  );
908
- const watchMirrorSync = useCallback2(
944
+ const watchMirrorSync = useCallback3(
909
945
  (placeholderId, expectedTicketId) => {
910
946
  const prior = watcherControllersRef.current.get(placeholderId);
911
947
  if (prior) prior.abort();
@@ -952,7 +988,7 @@ function useTicketActions(options) {
952
988
  [queryClient, removeOptimistic, toast2]
953
989
  );
954
990
  const lastUpdateErrorRef = useRef2(null);
955
- const surfaceError = useCallback2(
991
+ const surfaceError = useCallback3(
956
992
  (err, action) => {
957
993
  const mapped = mapTicketActionError(err);
958
994
  lastUpdateErrorRef.current = mapped;
@@ -966,7 +1002,7 @@ function useTicketActions(options) {
966
1002
  },
967
1003
  [toast2, onSupportSystemDown]
968
1004
  );
969
- const submitTicket = useCallback2(
1005
+ const submitTicket = useCallback3(
970
1006
  async (input) => {
971
1007
  if (formInFlightRef.current) return false;
972
1008
  formInFlightRef.current = true;
@@ -1035,7 +1071,7 @@ function useTicketActions(options) {
1035
1071
  surfaceError
1036
1072
  ]
1037
1073
  );
1038
- const updateTicket = useCallback2(
1074
+ const updateTicket = useCallback3(
1039
1075
  async (ticket, serverArgs, successCopy, action) => {
1040
1076
  if (busyRowsRef.current.has(ticket.id)) return false;
1041
1077
  setRowBusy(ticket.id, true);
@@ -1081,7 +1117,7 @@ function useTicketActions(options) {
1081
1117
  // and cascade-recreate addNote/closeTicket/etc.
1082
1118
  [setRowBusy, enqueue, executeTicketAction, queryClient, toast2, surfaceError, removeTicketFromCache]
1083
1119
  );
1084
- const sendMessage = useCallback2(
1120
+ const sendMessage = useCallback3(
1085
1121
  async (ticket, text, attachments) => {
1086
1122
  const trimmed = text.trim();
1087
1123
  const hasText = trimmed.length > 0;
@@ -1110,7 +1146,7 @@ function useTicketActions(options) {
1110
1146
  },
1111
1147
  [updateTicket, clearReplyError, setReplyError]
1112
1148
  );
1113
- const closeTicket = useCallback2(
1149
+ const closeTicket = useCallback3(
1114
1150
  (ticket, resolution) => updateTicket(
1115
1151
  ticket,
1116
1152
  {
@@ -1122,7 +1158,7 @@ function useTicketActions(options) {
1122
1158
  ),
1123
1159
  [updateTicket]
1124
1160
  );
1125
- const reopenTicket = useCallback2(
1161
+ const reopenTicket = useCallback3(
1126
1162
  (ticket) => updateTicket(ticket, { status: "OPEN" }, TOAST_COPY.reopen_success, "reopen ticket"),
1127
1163
  [updateTicket]
1128
1164
  );
@@ -1274,14 +1310,14 @@ function resolveErrorCode(bodyCode, status) {
1274
1310
  }
1275
1311
 
1276
1312
  // src/components/tickets/ticket-center.tsx
1277
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1313
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1278
1314
  function TicketCenter({ toast: toast2 = toast } = {}) {
1279
1315
  const identity = useChatIdentity();
1280
1316
  if (identity.isLoading) {
1281
- return /* @__PURE__ */ jsx5(TicketCenterSkeleton, {});
1317
+ return /* @__PURE__ */ jsx6(TicketCenterSkeleton, {});
1282
1318
  }
1283
1319
  if (identity.authTier === "anon" || !identity.user?.email) {
1284
- return /* @__PURE__ */ jsx5(
1320
+ return /* @__PURE__ */ jsx6(
1285
1321
  EmptyState,
1286
1322
  {
1287
1323
  type: "generic",
@@ -1291,7 +1327,7 @@ function TicketCenter({ toast: toast2 = toast } = {}) {
1291
1327
  }
1292
1328
  );
1293
1329
  }
1294
- return /* @__PURE__ */ jsx5(
1330
+ return /* @__PURE__ */ jsx6(
1295
1331
  TicketCenterAuthed,
1296
1332
  {
1297
1333
  toast: toast2,
@@ -1310,14 +1346,14 @@ function TicketCenterAuthed({
1310
1346
  const [optimisticTickets, setOptimisticTickets] = useState4([]);
1311
1347
  const [expandedTicketId, setExpandedTicketId] = useState4(null);
1312
1348
  const [supportSystemDown, setSupportSystemDown] = useState4(false);
1313
- const prependOptimistic = useCallback3((placeholder) => {
1349
+ const prependOptimistic = useCallback4((placeholder) => {
1314
1350
  setOptimisticTickets((prev) => [placeholder, ...prev]);
1315
1351
  }, []);
1316
- const removeOptimistic = useCallback3((placeholderId) => {
1352
+ const removeOptimistic = useCallback4((placeholderId) => {
1317
1353
  setOptimisticTickets((prev) => prev.filter((t) => t.id !== placeholderId));
1318
1354
  setExpandedTicketId((prev) => prev === placeholderId ? null : prev);
1319
1355
  }, []);
1320
- const removeTicketFromCache = useCallback3(
1356
+ const removeTicketFromCache = useCallback4(
1321
1357
  (ticketId) => {
1322
1358
  queryClient.setQueriesData(
1323
1359
  { queryKey: ["tickets"] },
@@ -1334,12 +1370,12 @@ function TicketCenterAuthed({
1334
1370
  toast: toast2,
1335
1371
  onSupportSystemDown: () => setSupportSystemDown(true)
1336
1372
  });
1337
- const toggleRow = useCallback3((id) => {
1373
+ const toggleRow = useCallback4((id) => {
1338
1374
  setExpandedTicketId((prev) => prev === id ? null : id);
1339
1375
  }, []);
1340
1376
  const merged = [...optimisticTickets, ...tickets];
1341
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-6", children: [
1342
- /* @__PURE__ */ jsx5(
1377
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-6", children: [
1378
+ /* @__PURE__ */ jsx6(
1343
1379
  TicketOpenForm,
1344
1380
  {
1345
1381
  onSubmit: (input) => actions.submitTicket(input),
@@ -1347,15 +1383,15 @@ function TicketCenterAuthed({
1347
1383
  supportSystemDown
1348
1384
  }
1349
1385
  ),
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: [
1386
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
1387
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between gap-3", children: [
1388
+ /* @__PURE__ */ jsx6("p", { className: "text-xs font-medium text-ods-text-secondary uppercase tracking-wider", children: "Your Current Tickets" }),
1389
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-3 text-xs text-ods-text-secondary", children: [
1390
+ lastUpdatedAt && /* @__PURE__ */ jsxs5("span", { children: [
1355
1391
  "Updated ",
1356
1392
  formatRelativeTime(new Date(lastUpdatedAt))
1357
1393
  ] }),
1358
- /* @__PURE__ */ jsx5(
1394
+ /* @__PURE__ */ jsx6(
1359
1395
  Button,
1360
1396
  {
1361
1397
  type: "button",
@@ -1364,12 +1400,12 @@ function TicketCenterAuthed({
1364
1400
  onClick: refetch,
1365
1401
  disabled: isFetching,
1366
1402
  "aria-label": "Refresh ticket list",
1367
- leftIcon: /* @__PURE__ */ jsx5(RefreshCw, { className: "h-4 w-4" })
1403
+ leftIcon: /* @__PURE__ */ jsx6(RefreshCw, { className: "h-4 w-4" })
1368
1404
  }
1369
1405
  )
1370
1406
  ] })
1371
1407
  ] }),
1372
- isLoading ? /* @__PURE__ */ jsx5(TicketListSkeleton, {}) : merged.length === 0 ? /* @__PURE__ */ jsx5(Card, { className: "p-6", children: /* @__PURE__ */ jsx5(
1408
+ isLoading ? /* @__PURE__ */ jsx6(TicketListSkeleton, {}) : merged.length === 0 ? /* @__PURE__ */ jsx6(Card, { className: "p-6", children: /* @__PURE__ */ jsx6(
1373
1409
  EmptyState,
1374
1410
  {
1375
1411
  type: "generic",
@@ -1377,7 +1413,7 @@ function TicketCenterAuthed({
1377
1413
  description: "Open one above to start the conversation.",
1378
1414
  showCTA: false
1379
1415
  }
1380
- ) }) : /* @__PURE__ */ jsx5(Card, { className: "overflow-hidden", children: merged.map((ticket) => /* @__PURE__ */ jsx5(
1416
+ ) }) : /* @__PURE__ */ jsx6(Card, { className: "overflow-hidden", children: merged.map((ticket) => /* @__PURE__ */ jsx6(
1381
1417
  TicketRow,
1382
1418
  {
1383
1419
  ticket,
@@ -1396,34 +1432,34 @@ function TicketCenterAuthed({
1396
1432
  ] });
1397
1433
  }
1398
1434
  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" })
1435
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-6", children: [
1436
+ /* @__PURE__ */ jsxs5(Card, { className: "p-6", children: [
1437
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-7 w-48 mb-4" }),
1438
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-10 w-full mb-3" }),
1439
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-24 w-full" })
1404
1440
  ] }),
1405
- /* @__PURE__ */ jsx5(TicketListSkeleton, {})
1441
+ /* @__PURE__ */ jsx6(TicketListSkeleton, {})
1406
1442
  ] });
1407
1443
  }
1408
1444
  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" })
1445
+ 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: [
1446
+ /* @__PURE__ */ jsxs5("div", { className: "flex-1 flex flex-col gap-2", children: [
1447
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-4 w-2/3" }),
1448
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-3 w-full" })
1413
1449
  ] }),
1414
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-8 w-20" }),
1415
- /* @__PURE__ */ jsx5(Skeleton, { className: "h-8 w-16" })
1450
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-8 w-20" }),
1451
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-8 w-16" })
1416
1452
  ] }, i)) });
1417
1453
  }
1418
1454
 
1419
1455
  // src/components/tickets/help-center-list.tsx
1420
- import { useCallback as useCallback5, useState as useState6 } from "react";
1456
+ import { useCallback as useCallback6, useState as useState6 } from "react";
1421
1457
  import { useQueryClient as useQueryClient3 } from "@tanstack/react-query";
1422
1458
  init_unified_pagination();
1423
1459
 
1424
1460
  // 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";
1461
+ import { useCallback as useCallback5, useEffect as useEffect2, useRef as useRef3 } from "react";
1462
+ import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1427
1463
  var STICKY_HEADER_OFFSET_PX = 96;
1428
1464
  function HelpCenterCard({
1429
1465
  ticket,
@@ -1448,25 +1484,20 @@ function HelpCenterCard({
1448
1484
  const isExpandable = !optimistic;
1449
1485
  const isExpanded = expanded && isExpandable;
1450
1486
  const rowRef = useRef3(null);
1451
- const handleClick = useCallback4(() => {
1487
+ const handleClick = useCallback5(() => {
1452
1488
  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
1489
  }, [onToggle, ticket.id]);
1468
- const rightBadges = /* @__PURE__ */ jsxs5(Fragment, { children: [
1469
- /* @__PURE__ */ jsx6(
1490
+ useEffect2(() => {
1491
+ if (!isExpanded) return;
1492
+ const raf = requestAnimationFrame(() => {
1493
+ scrollElementIntoView(rowRef.current, {
1494
+ headerOffset: STICKY_HEADER_OFFSET_PX
1495
+ });
1496
+ });
1497
+ return () => cancelAnimationFrame(raf);
1498
+ }, [isExpanded]);
1499
+ const rightBadges = /* @__PURE__ */ jsxs6(Fragment, { children: [
1500
+ /* @__PURE__ */ jsx7(
1470
1501
  StatusBadge,
1471
1502
  {
1472
1503
  text: rawStatus,
@@ -1475,7 +1506,7 @@ function HelpCenterCard({
1475
1506
  className: "border border-ods-border"
1476
1507
  }
1477
1508
  ),
1478
- priority && /* @__PURE__ */ jsx6(
1509
+ priority && /* @__PURE__ */ jsx7(
1479
1510
  StatusBadge,
1480
1511
  {
1481
1512
  text: priority,
@@ -1485,7 +1516,7 @@ function HelpCenterCard({
1485
1516
  }
1486
1517
  )
1487
1518
  ] });
1488
- return /* @__PURE__ */ jsxs5(
1519
+ return /* @__PURE__ */ jsxs6(
1489
1520
  "div",
1490
1521
  {
1491
1522
  ref: rowRef,
@@ -1493,7 +1524,7 @@ function HelpCenterCard({
1493
1524
  className: `border-b border-ods-border last:border-b-0 ${optimistic ? "opacity-60" : ""}`,
1494
1525
  "aria-busy": optimistic || void 0,
1495
1526
  children: [
1496
- /* @__PURE__ */ jsx6(
1527
+ /* @__PURE__ */ jsx7(
1497
1528
  "button",
1498
1529
  {
1499
1530
  type: "button",
@@ -1502,7 +1533,7 @@ function HelpCenterCard({
1502
1533
  "aria-expanded": isExpandable ? isExpanded : void 0,
1503
1534
  "aria-controls": isExpanded ? `help-center-drawer-${ticket.id}` : void 0,
1504
1535
  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(
1536
+ children: /* @__PURE__ */ jsx7(
1506
1537
  DevCardRowContent,
1507
1538
  {
1508
1539
  title,
@@ -1514,7 +1545,7 @@ function HelpCenterCard({
1514
1545
  )
1515
1546
  }
1516
1547
  ),
1517
- isExpanded && /* @__PURE__ */ jsx6("div", { id: `help-center-drawer-${ticket.id}`, children: /* @__PURE__ */ jsx6(
1548
+ isExpanded && /* @__PURE__ */ jsx7("div", { id: `help-center-drawer-${ticket.id}`, children: /* @__PURE__ */ jsx7(
1518
1549
  TicketDetailDrawer,
1519
1550
  {
1520
1551
  ticket,
@@ -1540,31 +1571,31 @@ function mapPriorityScheme(priority) {
1540
1571
 
1541
1572
  // src/components/tickets/help-center-create-form.tsx
1542
1573
  import { useState as useState5 } from "react";
1543
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1574
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1544
1575
  var SUBJECT_MAX_CHARS = 200;
1545
1576
  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" })
1577
+ 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: [
1578
+ /* @__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" }) }),
1579
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col flex-grow space-y-4 md:space-y-6", children: [
1580
+ /* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
1581
+ /* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
1582
+ /* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
1583
+ /* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
1584
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col", children: [
1585
+ /* @__PURE__ */ jsx8("div", { className: "h-[27px] w-20 bg-ods-border rounded animate-pulse mb-1" }),
1586
+ /* @__PURE__ */ jsx8("div", { className: "h-12 w-full bg-ods-border rounded animate-pulse" })
1556
1587
  ] }),
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" })
1588
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col flex-grow", children: [
1589
+ /* @__PURE__ */ jsx8("div", { className: "h-[27px] w-32 bg-ods-border rounded animate-pulse mb-1" }),
1590
+ /* @__PURE__ */ jsx8("div", { className: "h-24 w-full bg-ods-border rounded animate-pulse flex-grow" })
1560
1591
  ] }),
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" })
1592
+ /* @__PURE__ */ jsx8("div", { className: "flex flex-col gap-2", children: /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
1593
+ /* @__PURE__ */ jsx8("div", { className: "h-7 w-7 bg-ods-border rounded animate-pulse shrink-0" }),
1594
+ /* @__PURE__ */ jsx8("div", { className: "h-4 w-40 bg-ods-border rounded animate-pulse" })
1564
1595
  ] }) }),
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" })
1596
+ /* @__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: [
1597
+ /* @__PURE__ */ jsx8("div", { className: "h-4 w-72 bg-ods-border rounded animate-pulse" }),
1598
+ /* @__PURE__ */ jsx8("div", { className: "h-12 w-32 bg-ods-border rounded animate-pulse" })
1568
1599
  ] })
1569
1600
  ] })
1570
1601
  ] });
@@ -1577,12 +1608,12 @@ function HelpCenterCreateForm({
1577
1608
  }) {
1578
1609
  const [subject, setSubject] = useState5("");
1579
1610
  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: [
1611
+ const subjectField = /* @__PURE__ */ jsxs7("div", { className: "flex flex-col", children: [
1612
+ /* @__PURE__ */ jsxs7(Label, { htmlFor: "help-center-subject", children: [
1582
1613
  "Subject",
1583
- /* @__PURE__ */ jsx7("span", { className: "text-ods-accent", children: "*" })
1614
+ /* @__PURE__ */ jsx8("span", { className: "text-ods-accent", children: "*" })
1584
1615
  ] }),
1585
- /* @__PURE__ */ jsx7(
1616
+ /* @__PURE__ */ jsx8(
1586
1617
  Input,
1587
1618
  {
1588
1619
  id: "help-center-subject",
@@ -1600,7 +1631,7 @@ function HelpCenterCreateForm({
1600
1631
  className: "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12"
1601
1632
  }
1602
1633
  ),
1603
- subjectError && /* @__PURE__ */ jsx7(
1634
+ subjectError && /* @__PURE__ */ jsx8(
1604
1635
  "span",
1605
1636
  {
1606
1637
  id: "help-center-subject-error",
@@ -1609,7 +1640,7 @@ function HelpCenterCreateForm({
1609
1640
  }
1610
1641
  )
1611
1642
  ] });
1612
- return /* @__PURE__ */ jsx7(
1643
+ return /* @__PURE__ */ jsx8(
1613
1644
  ContactForm,
1614
1645
  {
1615
1646
  title: "Open a new ticket",
@@ -1646,7 +1677,7 @@ function HelpCenterCreateForm({
1646
1677
  }
1647
1678
 
1648
1679
  // src/components/tickets/help-center-list.tsx
1649
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1680
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1650
1681
  function HelpCenterList({ toast: toast2 = toast } = {}) {
1651
1682
  const identity = useChatIdentity();
1652
1683
  const searchParams = useSearchParams();
@@ -1654,13 +1685,14 @@ function HelpCenterList({ toast: toast2 = toast } = {}) {
1654
1685
  const pathname = usePathname();
1655
1686
  const search = searchParams.get("search") || "";
1656
1687
  const status = searchParams.get("status") || "all";
1688
+ const ticketParam = searchParams.get("ticket") || "";
1657
1689
  const rawPage = Number(searchParams.get("page"));
1658
1690
  const page = Number.isFinite(rawPage) && rawPage > 0 ? Math.floor(rawPage) : 1;
1659
1691
  if (identity.isLoading) {
1660
- return /* @__PURE__ */ jsx8(DevSectionPage, { sectionKey: "tickets", preControls: /* @__PURE__ */ jsx8(HelpCenterCreateFormSkeleton, {}), children: /* @__PURE__ */ jsx8(DevCardRowSkeletonList, {}) });
1692
+ return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", preControls: /* @__PURE__ */ jsx9(HelpCenterCreateFormSkeleton, {}), children: /* @__PURE__ */ jsx9(DevCardRowSkeletonList, {}) });
1661
1693
  }
1662
1694
  if (identity.authTier === "anon" || !identity.user?.email) {
1663
- return /* @__PURE__ */ jsx8(DevSectionPage, { sectionKey: "tickets", children: /* @__PURE__ */ jsx8(
1695
+ return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", children: /* @__PURE__ */ jsx9(
1664
1696
  EmptyState,
1665
1697
  {
1666
1698
  type: "generic",
@@ -1672,12 +1704,13 @@ function HelpCenterList({ toast: toast2 = toast } = {}) {
1672
1704
  }
1673
1705
  const sessionName = [identity.user?.firstName, identity.user?.lastName].filter(Boolean).join(" ").trim() || identity.user?.email?.split("@")[0] || "Customer";
1674
1706
  const sessionEmail = identity.user.email;
1675
- return /* @__PURE__ */ jsx8(
1707
+ return /* @__PURE__ */ jsx9(
1676
1708
  HelpCenterListAuthed,
1677
1709
  {
1678
1710
  search,
1679
1711
  status,
1680
1712
  page,
1713
+ ticketParam,
1681
1714
  searchParams,
1682
1715
  router,
1683
1716
  pathname,
@@ -1691,6 +1724,7 @@ function HelpCenterListAuthed({
1691
1724
  search,
1692
1725
  status,
1693
1726
  page,
1727
+ ticketParam,
1694
1728
  searchParams,
1695
1729
  router,
1696
1730
  pathname,
@@ -1699,6 +1733,18 @@ function HelpCenterListAuthed({
1699
1733
  sessionEmail
1700
1734
  }) {
1701
1735
  const queryClient = useQueryClient3();
1736
+ const [optimisticTickets, setOptimisticTickets] = useState6([]);
1737
+ const [supportSystemDown, setSupportSystemDown] = useState6(false);
1738
+ const setOpenTicket = useCallback6(
1739
+ (externalId) => {
1740
+ const params = new URLSearchParams(searchParams.toString());
1741
+ if (externalId) params.set("ticket", externalId);
1742
+ else params.delete("ticket");
1743
+ const qs = params.toString();
1744
+ router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
1745
+ },
1746
+ [searchParams, router, pathname]
1747
+ );
1702
1748
  const { tickets, isLoading, isFetching, error, refetch, totalPages } = useTicketsList({
1703
1749
  // `sessionEmail` is drilled in from the parent — see the same
1704
1750
  // pattern + race-cause rationale documented in
@@ -1709,19 +1755,21 @@ function HelpCenterListAuthed({
1709
1755
  customerEmail: sessionEmail,
1710
1756
  search,
1711
1757
  status,
1712
- page
1758
+ page,
1759
+ // Live status: while a drawer is open, poll so an out-of-band HubSpot
1760
+ // status change (e.g. agent closes the ticket) flips the badge +
1761
+ // open/reopen affordance within one interval. Idle (no drawer) → no poll.
1762
+ // `ticketParam` (the open ticket's external_id) is the open signal.
1763
+ refetchInterval: ticketParam ? TICKET_LIVE_POLL_MS : false
1713
1764
  });
1714
- const [optimisticTickets, setOptimisticTickets] = useState6([]);
1715
- const [expandedTicketId, setExpandedTicketId] = useState6(null);
1716
- const [supportSystemDown, setSupportSystemDown] = useState6(false);
1717
- const prependOptimistic = useCallback5((placeholder) => {
1765
+ const expandedTicketId = ticketParam && tickets.find((t) => t.external_id === ticketParam)?.id || null;
1766
+ const prependOptimistic = useCallback6((placeholder) => {
1718
1767
  setOptimisticTickets((prev) => [placeholder, ...prev]);
1719
1768
  }, []);
1720
- const removeOptimistic = useCallback5((placeholderId) => {
1769
+ const removeOptimistic = useCallback6((placeholderId) => {
1721
1770
  setOptimisticTickets((prev) => prev.filter((t) => t.id !== placeholderId));
1722
- setExpandedTicketId((prev) => prev === placeholderId ? null : prev);
1723
1771
  }, []);
1724
- const removeTicketFromCache = useCallback5(
1772
+ const removeTicketFromCache = useCallback6(
1725
1773
  (ticketId) => {
1726
1774
  queryClient.setQueriesData(
1727
1775
  { queryKey: ["tickets"] },
@@ -1732,7 +1780,6 @@ function HelpCenterListAuthed({
1732
1780
  return { ...prev, tickets: nextTickets };
1733
1781
  }
1734
1782
  );
1735
- setExpandedTicketId((prev) => prev === ticketId ? null : prev);
1736
1783
  },
1737
1784
  [queryClient]
1738
1785
  );
@@ -1743,13 +1790,18 @@ function HelpCenterListAuthed({
1743
1790
  toast: toast2,
1744
1791
  onSupportSystemDown: () => setSupportSystemDown(true)
1745
1792
  });
1746
- const toggleRow = useCallback5((id) => {
1747
- setExpandedTicketId((prev) => prev === id ? null : id);
1748
- }, []);
1793
+ const toggleRow = useCallback6(
1794
+ (id) => {
1795
+ const t = tickets.find((x) => x.id === id);
1796
+ if (!t?.external_id) return;
1797
+ setOpenTicket(t.external_id === ticketParam ? null : t.external_id);
1798
+ },
1799
+ [tickets, ticketParam, setOpenTicket]
1800
+ );
1749
1801
  const merged = [...optimisticTickets, ...tickets];
1750
1802
  const hasActiveFilters = search !== "" || status !== "" && status !== "all";
1751
1803
  const hasResults = merged.length > 0;
1752
- const form = /* @__PURE__ */ jsx8(
1804
+ const form = /* @__PURE__ */ jsx9(
1753
1805
  HelpCenterCreateForm,
1754
1806
  {
1755
1807
  actions,
@@ -1758,22 +1810,22 @@ function HelpCenterListAuthed({
1758
1810
  supportSystemDown
1759
1811
  }
1760
1812
  );
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: [
1813
+ const body = /* @__PURE__ */ jsxs8("div", { className: "w-full flex flex-col gap-[40px]", children: [
1814
+ 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: [
1815
+ /* @__PURE__ */ jsxs8("p", { className: "text-ods-error text-base", children: [
1764
1816
  "Couldn\u2019t load your tickets. ",
1765
1817
  error.message
1766
1818
  ] }),
1767
- /* @__PURE__ */ jsx8(Button, { type: "button", variant: "accent", onClick: () => refetch(), children: "Retry" })
1819
+ /* @__PURE__ */ jsx9(Button, { type: "button", variant: "accent", onClick: () => refetch(), children: "Retry" })
1768
1820
  ] }),
1769
- !error && /* @__PURE__ */ jsx8("div", { className: "w-full", children: isLoading ? /* @__PURE__ */ jsx8(DevCardRowSkeletonList, {}) : !hasResults && isFetching ? (
1821
+ !error && /* @__PURE__ */ jsx9("div", { className: "w-full", children: isLoading ? /* @__PURE__ */ jsx9(DevCardRowSkeletonList, {}) : !hasResults && isFetching ? (
1770
1822
  // Bridge state — background refetch in flight and the
1771
1823
  // optimistic placeholder was just removed by the mutation
1772
1824
  // callback. Without this branch "No tickets yet" would flash
1773
1825
  // for ~50ms between `removeOptimistic` and the server
1774
1826
  // response landing.
1775
- /* @__PURE__ */ jsx8(DevCardRowSkeletonList, { rows: 1 })
1776
- ) : !hasResults ? hasActiveFilters ? /* @__PURE__ */ jsx8(
1827
+ /* @__PURE__ */ jsx9(DevCardRowSkeletonList, { rows: 1 })
1828
+ ) : !hasResults ? hasActiveFilters ? /* @__PURE__ */ jsx9(
1777
1829
  EmptyState,
1778
1830
  {
1779
1831
  type: "search",
@@ -1788,7 +1840,7 @@ function HelpCenterListAuthed({
1788
1840
  router.replace(`${pathname}?${params.toString()}`, { scroll: false });
1789
1841
  }
1790
1842
  }
1791
- ) : /* @__PURE__ */ jsx8(
1843
+ ) : /* @__PURE__ */ jsx9(
1792
1844
  EmptyState,
1793
1845
  {
1794
1846
  type: "generic",
@@ -1805,7 +1857,7 @@ function HelpCenterListAuthed({
1805
1857
  // hidden) instead of bubbling up to the window. `clip`
1806
1858
  // keeps the visual clip but NOT the scroll-container
1807
1859
  // 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(
1860
+ /* @__PURE__ */ jsx9("div", { className: "bg-ods-card border border-ods-border rounded-[6px] overflow-clip w-full", children: merged.map((ticket) => /* @__PURE__ */ jsx9(
1809
1861
  HelpCenterCard,
1810
1862
  {
1811
1863
  ticket,
@@ -1816,16 +1868,16 @@ function HelpCenterListAuthed({
1816
1868
  onSendMessage: actions.sendMessage,
1817
1869
  onClose: actions.closeTicket,
1818
1870
  onReopen: actions.reopenTicket,
1819
- onActionCollapsed: () => setExpandedTicketId(null),
1871
+ onActionCollapsed: () => setOpenTicket(null),
1820
1872
  replyError: actions.replyErrorFor(ticket.external_id),
1821
1873
  onClearReplyError: () => actions.clearReplyError(ticket.external_id)
1822
1874
  },
1823
1875
  ticket.id
1824
1876
  )) })
1825
1877
  ) }),
1826
- !error && totalPages > 1 && /* @__PURE__ */ jsx8(UnifiedPagination, { currentPage: page, totalPages })
1878
+ !error && totalPages > 1 && /* @__PURE__ */ jsx9(UnifiedPagination, { currentPage: page, totalPages })
1827
1879
  ] });
1828
- return /* @__PURE__ */ jsx8(DevSectionPage, { sectionKey: "tickets", preControls: form, children: body });
1880
+ return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", preControls: form, children: body });
1829
1881
  }
1830
1882
  export {
1831
1883
  HelpCenterCard,
@@ -1837,6 +1889,7 @@ export {
1837
1889
  TicketDetailDrawer,
1838
1890
  TicketLinkedDeliveryCard,
1839
1891
  TicketOpenForm,
1892
+ TicketReplyComposer,
1840
1893
  TicketRow,
1841
1894
  isOptimistic,
1842
1895
  mapTicketActionError,