@flamingo-stack/openframe-frontend-core 0.0.315 → 0.0.316

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 (163) hide show
  1. package/dist/{chunk-7U4YFQX2.js → chunk-2Y4DLBFO.js} +96 -92
  2. package/dist/{chunk-7U4YFQX2.js.map → chunk-2Y4DLBFO.js.map} +1 -1
  3. package/dist/{chunk-HATCNFQL.cjs → chunk-4MCMPYEM.cjs} +12 -12
  4. package/dist/{chunk-HATCNFQL.cjs.map → chunk-4MCMPYEM.cjs.map} +1 -1
  5. package/dist/{chunk-VY5YF4B7.js → chunk-4NVA6W3J.js} +27 -22
  6. package/dist/chunk-4NVA6W3J.js.map +1 -0
  7. package/dist/chunk-4V3TCOFC.cjs +394 -0
  8. package/dist/chunk-4V3TCOFC.cjs.map +1 -0
  9. package/dist/{chunk-A2H6TFS4.cjs → chunk-63A53WQN.cjs} +33 -33
  10. package/dist/{chunk-A2H6TFS4.cjs.map → chunk-63A53WQN.cjs.map} +1 -1
  11. package/dist/{chunk-6W54MBU2.js → chunk-64DZ2J7Q.js} +5 -5
  12. package/dist/{chunk-47JZOP7Y.js → chunk-6KERXOFE.js} +3 -3
  13. package/dist/{chunk-JALO4TAZ.js → chunk-AI5X5JTD.js} +4 -4
  14. package/dist/chunk-CSLMCBZV.js +1464 -0
  15. package/dist/chunk-CSLMCBZV.js.map +1 -0
  16. package/dist/{chunk-BSAFGQVW.cjs → chunk-CUNMBP3A.cjs} +13 -13
  17. package/dist/{chunk-BSAFGQVW.cjs.map → chunk-CUNMBP3A.cjs.map} +1 -1
  18. package/dist/{chunk-TVNILN2F.cjs → chunk-DHVL36CA.cjs} +40 -40
  19. package/dist/{chunk-TVNILN2F.cjs.map → chunk-DHVL36CA.cjs.map} +1 -1
  20. package/dist/chunk-FCEVVNWY.cjs +1916 -0
  21. package/dist/chunk-FCEVVNWY.cjs.map +1 -0
  22. package/dist/chunk-FOVX3W3C.cjs +1464 -0
  23. package/dist/chunk-FOVX3W3C.cjs.map +1 -0
  24. package/dist/{chunk-4D37W55K.js → chunk-GHVVOST5.js} +95 -116
  25. package/dist/chunk-GHVVOST5.js.map +1 -0
  26. package/dist/{chunk-TRSDXD23.js → chunk-JAZM3A7E.js} +2 -2
  27. package/dist/{chunk-TK6OABYF.js → chunk-JEBL5PQK.js} +21 -35
  28. package/dist/{chunk-TK6OABYF.js.map → chunk-JEBL5PQK.js.map} +1 -1
  29. package/dist/{chunk-5ATH263N.cjs → chunk-L5JSGNT3.cjs} +35 -35
  30. package/dist/{chunk-5ATH263N.cjs.map → chunk-L5JSGNT3.cjs.map} +1 -1
  31. package/dist/{chunk-TQ7CMFSY.cjs → chunk-LAMDFGE3.cjs} +41 -36
  32. package/dist/chunk-LAMDFGE3.cjs.map +1 -0
  33. package/dist/{chunk-V4IIBNTA.js → chunk-LQHMXPOJ.js} +5 -5
  34. package/dist/{chunk-LGLPNWS6.cjs → chunk-LWNPMLIH.cjs} +3 -3
  35. package/dist/{chunk-LGLPNWS6.cjs.map → chunk-LWNPMLIH.cjs.map} +1 -1
  36. package/dist/chunk-M3NULYCR.js +1916 -0
  37. package/dist/chunk-M3NULYCR.js.map +1 -0
  38. package/dist/{chunk-MOOV4ORG.js → chunk-OKGZK6TT.js} +3 -3
  39. package/dist/{chunk-WFHNXCI3.cjs → chunk-OLEW7FYZ.cjs} +123 -144
  40. package/dist/chunk-OLEW7FYZ.cjs.map +1 -0
  41. package/dist/chunk-PIJ4JLJU.js +394 -0
  42. package/dist/chunk-PIJ4JLJU.js.map +1 -0
  43. package/dist/{chunk-E4CQ4RUG.js → chunk-Q4AMYLKX.js} +11 -11
  44. package/dist/{chunk-FQOTC3UU.cjs → chunk-QJGRP2YE.cjs} +4 -4
  45. package/dist/{chunk-FQOTC3UU.cjs.map → chunk-QJGRP2YE.cjs.map} +1 -1
  46. package/dist/{chunk-ZPK5HW7B.cjs → chunk-UGDGUO26.cjs} +3 -3
  47. package/dist/{chunk-ZPK5HW7B.cjs.map → chunk-UGDGUO26.cjs.map} +1 -1
  48. package/dist/{chunk-QW6OL4NY.cjs → chunk-VCE3ZEN3.cjs} +5 -5
  49. package/dist/{chunk-QW6OL4NY.cjs.map → chunk-VCE3ZEN3.cjs.map} +1 -1
  50. package/dist/{chunk-2JPSWDSM.cjs → chunk-XAQJ4ZLY.cjs} +447 -443
  51. package/dist/{chunk-2JPSWDSM.cjs.map → chunk-XAQJ4ZLY.cjs.map} +1 -1
  52. package/dist/{chunk-2MLMZAK4.js → chunk-YFGDZFUG.js} +4 -4
  53. package/dist/{chunk-VFIWQGJZ.js → chunk-Z3YORGG4.js} +2 -2
  54. package/dist/{chunk-OSEKWT6X.cjs → chunk-ZYGVJXJ5.cjs} +33 -47
  55. package/dist/chunk-ZYGVJXJ5.cjs.map +1 -0
  56. package/dist/components/case-studies/index.cjs +18 -18
  57. package/dist/components/case-studies/index.cjs.map +1 -1
  58. package/dist/components/case-studies/index.js +8 -8
  59. package/dist/components/chat/index.cjs +8 -8
  60. package/dist/components/chat/index.js +7 -7
  61. package/dist/components/contact/index.cjs +9 -9
  62. package/dist/components/contact/index.js +8 -8
  63. package/dist/components/docs/doc-viewer.d.ts +4 -0
  64. package/dist/components/docs/doc-viewer.d.ts.map +1 -1
  65. package/dist/components/docs/index.cjs +11 -11
  66. package/dist/components/docs/index.js +10 -10
  67. package/dist/components/embeds/index.cjs +9 -9
  68. package/dist/components/embeds/index.js +8 -8
  69. package/dist/components/faq/faq-document-page.d.ts +18 -20
  70. package/dist/components/faq/faq-document-page.d.ts.map +1 -1
  71. package/dist/components/faq/index.cjs +10 -10
  72. package/dist/components/faq/index.js +9 -9
  73. package/dist/components/features/index.cjs +8 -8
  74. package/dist/components/features/index.js +7 -7
  75. package/dist/components/help-center-pages/delivery-page.d.ts +27 -0
  76. package/dist/components/help-center-pages/delivery-page.d.ts.map +1 -0
  77. package/dist/components/help-center-pages/index.cjs +164 -0
  78. package/dist/components/help-center-pages/index.cjs.map +1 -0
  79. package/dist/components/help-center-pages/index.d.ts +25 -0
  80. package/dist/components/help-center-pages/index.d.ts.map +1 -0
  81. package/dist/components/help-center-pages/index.js +164 -0
  82. package/dist/components/help-center-pages/index.js.map +1 -0
  83. package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts +41 -0
  84. package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts.map +1 -0
  85. package/dist/components/help-center-pages/product-releases-list-page.d.ts +34 -0
  86. package/dist/components/help-center-pages/product-releases-list-page.d.ts.map +1 -0
  87. package/dist/components/help-center-pages/roadmap-page.d.ts +40 -0
  88. package/dist/components/help-center-pages/roadmap-page.d.ts.map +1 -0
  89. package/dist/components/icons/index.cjs +3 -3
  90. package/dist/components/icons/index.js +2 -2
  91. package/dist/components/index.cjs +177 -1555
  92. package/dist/components/index.cjs.map +1 -1
  93. package/dist/components/index.js +348 -1726
  94. package/dist/components/index.js.map +1 -1
  95. package/dist/components/layout/page-layout.d.ts +4 -1
  96. package/dist/components/layout/page-layout.d.ts.map +1 -1
  97. package/dist/components/layout/title-block.d.ts +5 -1
  98. package/dist/components/layout/title-block.d.ts.map +1 -1
  99. package/dist/components/navigation/index.cjs +8 -8
  100. package/dist/components/navigation/index.js +7 -7
  101. package/dist/components/onboarding-guides/index.cjs +15 -364
  102. package/dist/components/onboarding-guides/index.cjs.map +1 -1
  103. package/dist/components/onboarding-guides/index.js +20 -369
  104. package/dist/components/onboarding-guides/index.js.map +1 -1
  105. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +9 -1
  106. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  107. package/dist/components/related-content/index.cjs +10 -10
  108. package/dist/components/related-content/index.js +9 -9
  109. package/dist/components/shared/dev-section/dev-section-page.d.ts +7 -1
  110. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
  111. package/dist/components/shared/dev-section/dev-section-view.d.ts +7 -1
  112. package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
  113. package/dist/components/shared/legal-document/legal-document-page.d.ts +5 -1
  114. package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
  115. package/dist/components/shared/product-release/release-detail-page.d.ts +11 -2
  116. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  117. package/dist/components/tickets/help-center-list.d.ts +5 -1
  118. package/dist/components/tickets/help-center-list.d.ts.map +1 -1
  119. package/dist/components/tickets/index.cjs +15 -1882
  120. package/dist/components/tickets/index.cjs.map +1 -1
  121. package/dist/components/tickets/index.js +28 -1895
  122. package/dist/components/tickets/index.js.map +1 -1
  123. package/dist/components/ui/file-manager/index.cjs +53 -53
  124. package/dist/components/ui/file-manager/index.cjs.map +1 -1
  125. package/dist/components/ui/file-manager/index.js +4 -4
  126. package/dist/components/ui/index.cjs +8 -8
  127. package/dist/components/ui/index.cjs.map +1 -1
  128. package/dist/components/ui/index.js +7 -7
  129. package/dist/hooks/index.cjs +5 -5
  130. package/dist/hooks/index.js +4 -4
  131. package/dist/index.cjs +10 -10
  132. package/dist/index.cjs.map +1 -1
  133. package/dist/index.js +9 -9
  134. package/package.json +7 -1
  135. package/src/components/docs/doc-viewer.tsx +21 -34
  136. package/src/components/faq/faq-document-page.tsx +33 -60
  137. package/src/components/help-center-pages/delivery-page.tsx +45 -0
  138. package/src/components/help-center-pages/index.ts +41 -0
  139. package/src/components/help-center-pages/onboarding-guides-catalog-page.tsx +66 -0
  140. package/src/components/help-center-pages/product-releases-list-page.tsx +58 -0
  141. package/src/components/help-center-pages/roadmap-page.tsx +68 -0
  142. package/src/components/layout/page-layout.tsx +11 -0
  143. package/src/components/layout/title-block.tsx +15 -2
  144. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +30 -19
  145. package/src/components/shared/dev-section/dev-section-page.tsx +29 -19
  146. package/src/components/shared/dev-section/dev-section-view.tsx +26 -19
  147. package/src/components/shared/legal-document/legal-document-page.tsx +19 -23
  148. package/src/components/shared/product-release/release-detail-page.tsx +36 -36
  149. package/src/components/tickets/help-center-list.tsx +11 -3
  150. package/dist/chunk-4D37W55K.js.map +0 -1
  151. package/dist/chunk-OSEKWT6X.cjs.map +0 -1
  152. package/dist/chunk-TQ7CMFSY.cjs.map +0 -1
  153. package/dist/chunk-VY5YF4B7.js.map +0 -1
  154. package/dist/chunk-WFHNXCI3.cjs.map +0 -1
  155. /package/dist/{chunk-6W54MBU2.js.map → chunk-64DZ2J7Q.js.map} +0 -0
  156. /package/dist/{chunk-47JZOP7Y.js.map → chunk-6KERXOFE.js.map} +0 -0
  157. /package/dist/{chunk-JALO4TAZ.js.map → chunk-AI5X5JTD.js.map} +0 -0
  158. /package/dist/{chunk-TRSDXD23.js.map → chunk-JAZM3A7E.js.map} +0 -0
  159. /package/dist/{chunk-V4IIBNTA.js.map → chunk-LQHMXPOJ.js.map} +0 -0
  160. /package/dist/{chunk-MOOV4ORG.js.map → chunk-OKGZK6TT.js.map} +0 -0
  161. /package/dist/{chunk-E4CQ4RUG.js.map → chunk-Q4AMYLKX.js.map} +0 -0
  162. /package/dist/{chunk-2MLMZAK4.js.map → chunk-YFGDZFUG.js.map} +0 -0
  163. /package/dist/{chunk-VFIWQGJZ.js.map → chunk-Z3YORGG4.js.map} +0 -0
@@ -0,0 +1,1916 @@
1
+ "use client";
2
+ import {
3
+ ContactForm
4
+ } from "./chunk-YFGDZFUG.js";
5
+ import {
6
+ DeliveryRow,
7
+ DevCardRowContent,
8
+ DevCardRowSkeletonList,
9
+ DevSectionPage,
10
+ EmptyState,
11
+ UnifiedPagination,
12
+ init_unified_pagination
13
+ } from "./chunk-4NVA6W3J.js";
14
+ import {
15
+ AlertDialog,
16
+ AlertDialogAction,
17
+ AlertDialogCancel,
18
+ AlertDialogContent,
19
+ AlertDialogDescription,
20
+ AlertDialogFooter,
21
+ AlertDialogHeader,
22
+ AlertDialogTitle,
23
+ Card,
24
+ ChatAttachmentAddButton,
25
+ ChatAttachmentChipStrip,
26
+ ChatInput,
27
+ ChatMessageRow,
28
+ ChatMessageRowSkeleton,
29
+ ChatTicketItem,
30
+ Label,
31
+ SquareAvatar,
32
+ StatusBadge,
33
+ Textarea,
34
+ TicketAttachmentsList,
35
+ devSectionAnchorId,
36
+ formatRelativeTime,
37
+ getStatusColorScheme,
38
+ useChatAttachments,
39
+ useChatIdentity
40
+ } from "./chunk-2Y4DLBFO.js";
41
+ import {
42
+ STICKY_HEADER_OFFSET_PX,
43
+ embedAuthedFetch,
44
+ scrollElementIntoView,
45
+ toast,
46
+ useScrollToHash
47
+ } from "./chunk-Q4AMYLKX.js";
48
+ import {
49
+ useRequiredChatRuntime
50
+ } from "./chunk-2FI3USTC.js";
51
+ import {
52
+ usePathname,
53
+ useRouter,
54
+ useSearchParams
55
+ } from "./chunk-PLJLE4A4.js";
56
+ import {
57
+ Button,
58
+ Input,
59
+ Skeleton,
60
+ init_button2 as init_button
61
+ } from "./chunk-AI5X5JTD.js";
62
+
63
+ // src/components/tickets/ticket-center.tsx
64
+ import { useCallback as useCallback4, useState as useState4 } from "react";
65
+ import { useQueryClient as useQueryClient2 } from "@tanstack/react-query";
66
+ init_button();
67
+ import { RefreshCw } from "lucide-react";
68
+
69
+ // src/components/tickets/ticket-open-form.tsx
70
+ import { useState } from "react";
71
+ init_button();
72
+
73
+ // src/components/tickets/types.ts
74
+ function isOptimistic(t) {
75
+ return t._optimistic === true;
76
+ }
77
+ var TICKET_TEXT_MAX_CHARS = 5e3;
78
+ var TICKET_LIVE_POLL_MS = 8e3;
79
+ var TOAST_COPY = {
80
+ open_success: { title: "Ticket opened", description: "We received your message and will follow up shortly." },
81
+ open_mirror_pending: { title: "Ticket opened", description: "Syncing \u2014 your ticket will appear momentarily." },
82
+ close_success: { title: "Ticket closed" },
83
+ reopen_success: { title: "Ticket reopened" },
84
+ comment_success: { title: "Comment added" },
85
+ attach_success: { title: "Files attached" }
86
+ // Failure variants are constructed dynamically from MappedTicketActionError.
87
+ };
88
+
89
+ // src/components/tickets/ticket-open-form.tsx
90
+ import { jsx, jsxs } from "react/jsx-runtime";
91
+ var COUNTER_VISIBLE_AT = Math.floor(TICKET_TEXT_MAX_CHARS * 0.8);
92
+ function TicketOpenForm({
93
+ onSubmit,
94
+ isSubmitting,
95
+ supportSystemDown
96
+ }) {
97
+ const [subject, setSubject] = useState("");
98
+ const [content, setContent] = useState("");
99
+ const { attachments, readyAttachments, hasInflightUploads, addFiles, removeAttachment, clear } = useChatAttachments();
100
+ const trimmedSubject = subject.trim();
101
+ const trimmedContent = content.trim();
102
+ const overCap = content.length > TICKET_TEXT_MAX_CHARS;
103
+ const showCounter = content.length >= COUNTER_VISIBLE_AT;
104
+ const canSubmit = !isSubmitting && !supportSystemDown && !hasInflightUploads && trimmedSubject.length > 0 && trimmedContent.length > 0 && !overCap;
105
+ const handleSubmit = async (e) => {
106
+ e.preventDefault();
107
+ if (!canSubmit) return;
108
+ const ok = await onSubmit({
109
+ subject: trimmedSubject,
110
+ content: trimmedContent,
111
+ attachments: readyAttachments
112
+ });
113
+ if (ok) {
114
+ setSubject("");
115
+ setContent("");
116
+ clear();
117
+ }
118
+ };
119
+ return /* @__PURE__ */ jsx(Card, { className: "p-6", children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col md:flex-row gap-6", children: [
120
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 md:max-w-md", children: [
121
+ /* @__PURE__ */ jsx("h2", { className: "text-2xl font-semibold text-ods-text-primary mb-2", children: "Need Support?" }),
122
+ /* @__PURE__ */ jsx("p", { className: "text-ods-text-secondary text-sm", children: "Can't find what you're looking for? Submit a support ticket below \u2014 we'll follow up shortly." }),
123
+ supportSystemDown && /* @__PURE__ */ jsx("p", { className: "mt-4 text-sm text-ods-error", children: "Support system temporarily unavailable. Please try again shortly." })
124
+ ] }),
125
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 flex flex-col gap-4", children: [
126
+ /* @__PURE__ */ jsxs("div", { children: [
127
+ /* @__PURE__ */ jsx(
128
+ "label",
129
+ {
130
+ htmlFor: "ticket-subject",
131
+ className: "block text-sm font-medium text-ods-text-primary mb-1",
132
+ children: "Ticket Subject"
133
+ }
134
+ ),
135
+ /* @__PURE__ */ jsx(
136
+ Input,
137
+ {
138
+ id: "ticket-subject",
139
+ type: "text",
140
+ placeholder: "Enter Subject Here",
141
+ value: subject,
142
+ onChange: (e) => setSubject(e.target.value),
143
+ disabled: isSubmitting || supportSystemDown,
144
+ maxLength: 200
145
+ }
146
+ )
147
+ ] }),
148
+ /* @__PURE__ */ jsxs("div", { children: [
149
+ /* @__PURE__ */ jsx(
150
+ "label",
151
+ {
152
+ htmlFor: "ticket-content",
153
+ className: "block text-sm font-medium text-ods-text-primary mb-1",
154
+ children: "Your Message"
155
+ }
156
+ ),
157
+ /* @__PURE__ */ jsx(
158
+ Textarea,
159
+ {
160
+ id: "ticket-content",
161
+ placeholder: "Describe your issue or question in detail...",
162
+ value: content,
163
+ onChange: (e) => setContent(e.target.value),
164
+ disabled: isSubmitting || supportSystemDown,
165
+ rows: 5,
166
+ className: "resize-none"
167
+ }
168
+ ),
169
+ showCounter && /* @__PURE__ */ jsxs(
170
+ "p",
171
+ {
172
+ className: `mt-1 text-xs text-right ${overCap ? "text-ods-error" : "text-ods-text-secondary"}`,
173
+ children: [
174
+ content.length,
175
+ "/",
176
+ TICKET_TEXT_MAX_CHARS
177
+ ]
178
+ }
179
+ )
180
+ ] }),
181
+ /* @__PURE__ */ jsx(
182
+ ChatAttachmentChipStrip,
183
+ {
184
+ attachments,
185
+ onRemove: removeAttachment,
186
+ disabled: isSubmitting || supportSystemDown
187
+ }
188
+ ),
189
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
190
+ /* @__PURE__ */ jsx(
191
+ ChatAttachmentAddButton,
192
+ {
193
+ attachmentsEnabled: !supportSystemDown,
194
+ attachmentsCount: attachments.length,
195
+ onAddFiles: addFiles,
196
+ disabled: isSubmitting
197
+ }
198
+ ),
199
+ /* @__PURE__ */ jsx(
200
+ Button,
201
+ {
202
+ type: "submit",
203
+ disabled: !canSubmit,
204
+ loading: isSubmitting,
205
+ children: "Open Ticket"
206
+ }
207
+ )
208
+ ] })
209
+ ] })
210
+ ] }) });
211
+ }
212
+
213
+ // src/components/tickets/ticket-row.tsx
214
+ import { useCallback as useCallback2, useRef } from "react";
215
+
216
+ // src/components/collapsible.tsx
217
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
218
+ var Collapsible = CollapsiblePrimitive.Root;
219
+ var CollapsibleContent2 = CollapsiblePrimitive.CollapsibleContent;
220
+
221
+ // src/components/tickets/ticket-detail-drawer.tsx
222
+ init_button();
223
+ import { useStickToBottom } from "use-stick-to-bottom";
224
+
225
+ // src/components/tickets/hooks/use-ticket-engagements.ts
226
+ import { useQuery } from "@tanstack/react-query";
227
+ var LIST_ENGAGEMENTS_ENDPOINT = "/api/chat/agent/list-engagements";
228
+ function useTicketEngagements(externalTicketId, enabled = true, refetchInterval = false) {
229
+ const identity = useChatIdentity();
230
+ const identityKey = identity.user?.email ?? "anon";
231
+ const listEngagementsEndpoint = useRequiredChatRuntime().endpoints.listEngagementsUrl ?? LIST_ENGAGEMENTS_ENDPOINT;
232
+ const fetchable = enabled && !!externalTicketId && !externalTicketId.startsWith("temp-");
233
+ const queryEnabled = fetchable && identity.authTier !== "anon" && !!identity.user?.email;
234
+ const query = useQuery({
235
+ queryKey: ["ticket-engagements", externalTicketId, identityKey],
236
+ enabled: queryEnabled,
237
+ // Caches OFF — same reasoning as `useTicketsList`. The conversation
238
+ // timeline must reflect HubSpot truth on every drawer-open; a stale
239
+ // window risks hiding a freshly-arrived agent reply.
240
+ staleTime: 0,
241
+ gcTime: 0,
242
+ refetchOnMount: "always",
243
+ refetchOnWindowFocus: true,
244
+ // Live conversation: poll while the caller opts in (drawer open). New
245
+ // agent replies + attachments appear within one interval without a
246
+ // manual refresh. `refetchIntervalInBackground` stays false (default)
247
+ // so polling pauses on a hidden tab.
248
+ refetchInterval,
249
+ queryFn: async () => {
250
+ const response = await embedAuthedFetch(listEngagementsEndpoint, {
251
+ method: "POST",
252
+ body: JSON.stringify({ ticket_id: externalTicketId })
253
+ });
254
+ if (!response.ok) {
255
+ const text = await response.text().catch(() => "");
256
+ throw new Error(`list-engagements failed: ${response.status} ${text.slice(0, 200)}`);
257
+ }
258
+ const body = await response.json();
259
+ return Array.isArray(body.engagements) ? body.engagements : [];
260
+ }
261
+ });
262
+ return {
263
+ engagements: query.data ?? [],
264
+ // Loading-state truth that prevents the "body → blink → skeleton → data"
265
+ // double-flash. The bug: `useChatIdentity` starts at anon defaults and
266
+ // resolves async, so on the first render `queryEnabled` is false and the
267
+ // OLD `queryEnabled && query.isLoading` returned FALSE — the panel rendered
268
+ // the ticket body, THEN identity resolved, the query enabled, isLoading
269
+ // flipped true → skeleton appeared (the blink), then data landed.
270
+ //
271
+ // Fix: for a fetchable ticket we are "loading" whenever we don't yet have
272
+ // the timeline to show — that includes the window while identity is still
273
+ // resolving (so we skeleton from the FIRST render, never the body) AND the
274
+ // cold query fetch (`data === undefined`). A background poll keeps
275
+ // `query.data` defined, so it never re-flashes the skeleton. Non-fetchable
276
+ // (optimistic/disabled) or a resolved-anon viewer → not loading.
277
+ isLoading: fetchable && (identity.isLoading || queryEnabled && query.data === void 0),
278
+ isFetching: query.isFetching,
279
+ error: query.error ?? null,
280
+ refetch: () => {
281
+ void query.refetch();
282
+ }
283
+ };
284
+ }
285
+
286
+ // src/components/tickets/ticket-linked-delivery-card.tsx
287
+ import { jsx as jsx2 } from "react/jsx-runtime";
288
+ function TicketLinkedDeliveryCard({
289
+ clickup,
290
+ className
291
+ }) {
292
+ const item = {
293
+ id: clickup.external_id,
294
+ title: clickup.title ?? "Linked delivery task",
295
+ description: clickup.description ?? "",
296
+ status: clickup.status ?? "unknown",
297
+ statusColor: clickup.status_color ?? "#87909e",
298
+ taskType: clickup.task_type ?? "Request",
299
+ customItemId: clickup.custom_item_id,
300
+ listNames: clickup.list_names,
301
+ dateOpened: clickup.date_opened ?? 0,
302
+ dateUpdated: clickup.date_updated ?? clickup.date_opened ?? Date.now(),
303
+ dateClosed: clickup.date_closed,
304
+ clickupUrl: clickup.clickup_url ?? ""
305
+ };
306
+ return /* @__PURE__ */ jsx2(
307
+ "div",
308
+ {
309
+ className: `rounded-md border border-ods-border bg-ods-bg overflow-hidden ${className ?? ""}`,
310
+ children: /* @__PURE__ */ jsx2(
311
+ DeliveryRow,
312
+ {
313
+ item,
314
+ href: clickup.delivery_href,
315
+ caption: "Linked delivery"
316
+ }
317
+ )
318
+ }
319
+ );
320
+ }
321
+
322
+ // src/components/tickets/ticket-reply-composer.tsx
323
+ init_button();
324
+ import { useCallback, useState as useState2 } from "react";
325
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
326
+ function TicketReplyComposer({
327
+ ticket,
328
+ busy,
329
+ supportSystemDown,
330
+ onSendMessage,
331
+ onClose
332
+ }) {
333
+ const [resolution, setResolution] = useState2("");
334
+ const [closeDialogOpen, setCloseDialogOpen] = useState2(false);
335
+ const attachments = useChatAttachments();
336
+ const ticketRef = { id: ticket.id, external_id: ticket.external_id };
337
+ const hasReadyFiles = attachments.readyAttachments.length > 0;
338
+ const handleSend = useCallback(
339
+ async (text) => {
340
+ const ref = { id: ticket.id, external_id: ticket.external_id };
341
+ const ok = await onSendMessage(ref, text.trim(), attachments.readyAttachments);
342
+ if (ok) attachments.clear();
343
+ return ok;
344
+ },
345
+ // Depend on the reactive projections, not the whole bag (a fresh object each
346
+ // render). `readyAttachments` is memo-stable; `clear` is callback-stable.
347
+ [
348
+ onSendMessage,
349
+ ticket.id,
350
+ ticket.external_id,
351
+ attachments.readyAttachments,
352
+ attachments.clear
353
+ ]
354
+ );
355
+ const confirmClose = async () => {
356
+ setCloseDialogOpen(false);
357
+ await onClose(ticketRef, resolution.trim() || void 0);
358
+ setResolution("");
359
+ };
360
+ const disabled = busy || supportSystemDown;
361
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2", children: [
362
+ /* @__PURE__ */ jsx3(
363
+ ChatAttachmentChipStrip,
364
+ {
365
+ attachments: attachments.attachments,
366
+ onRemove: attachments.removeAttachment,
367
+ disabled,
368
+ size: "compact"
369
+ }
370
+ ),
371
+ /* @__PURE__ */ jsx3(
372
+ ChatInput,
373
+ {
374
+ fullWidth: true,
375
+ autoFocus: true,
376
+ placeholder: "Type a reply\u2026",
377
+ sending: busy || attachments.hasInflightUploads,
378
+ disabled: supportSystemDown,
379
+ allowEmptySend: hasReadyFiles,
380
+ maxLength: TICKET_TEXT_MAX_CHARS,
381
+ onSend: handleSend
382
+ }
383
+ ),
384
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 w-full", children: [
385
+ !supportSystemDown && /* @__PURE__ */ jsx3(
386
+ ChatAttachmentAddButton,
387
+ {
388
+ attachmentsEnabled: true,
389
+ attachmentsCount: attachments.attachments.length,
390
+ onAddFiles: attachments.addFiles,
391
+ disabled,
392
+ size: "compact"
393
+ }
394
+ ),
395
+ /* @__PURE__ */ jsx3("div", { className: "flex-1 min-w-0" }),
396
+ /* @__PURE__ */ jsx3(
397
+ Button,
398
+ {
399
+ type: "button",
400
+ variant: "transparent",
401
+ size: "small",
402
+ onClick: () => setCloseDialogOpen(true),
403
+ disabled,
404
+ className: "text-ods-text-secondary hover:text-ods-text-primary",
405
+ children: "Close ticket"
406
+ }
407
+ )
408
+ ] }),
409
+ /* @__PURE__ */ jsx3(AlertDialog, { open: closeDialogOpen, onOpenChange: setCloseDialogOpen, children: /* @__PURE__ */ jsxs2(AlertDialogContent, { className: "bg-ods-card border-ods-border", children: [
410
+ /* @__PURE__ */ jsxs2(AlertDialogHeader, { children: [
411
+ /* @__PURE__ */ jsx3(AlertDialogTitle, { className: "text-ods-text-primary", children: "Close this ticket?" }),
412
+ /* @__PURE__ */ jsx3(AlertDialogDescription, { className: "text-ods-text-secondary", children: "Add an optional resolution note below. You can reopen the ticket later if needed." })
413
+ ] }),
414
+ /* @__PURE__ */ jsx3(
415
+ Textarea,
416
+ {
417
+ value: resolution,
418
+ onChange: (e) => setResolution(e.target.value),
419
+ placeholder: "Resolution (optional)",
420
+ rows: 3,
421
+ maxLength: TICKET_TEXT_MAX_CHARS,
422
+ className: "mt-2"
423
+ }
424
+ ),
425
+ /* @__PURE__ */ jsxs2(AlertDialogFooter, { children: [
426
+ /* @__PURE__ */ jsx3(
427
+ AlertDialogCancel,
428
+ {
429
+ disabled: busy,
430
+ className: "bg-transparent border-ods-border text-ods-text-primary hover:bg-ods-border",
431
+ children: "Cancel"
432
+ }
433
+ ),
434
+ /* @__PURE__ */ jsx3(
435
+ AlertDialogAction,
436
+ {
437
+ onClick: () => void confirmClose(),
438
+ disabled: busy,
439
+ className: "bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover",
440
+ children: "Close ticket"
441
+ }
442
+ )
443
+ ] })
444
+ ] }) })
445
+ ] });
446
+ }
447
+
448
+ // src/components/tickets/ticket-detail-drawer.tsx
449
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
450
+ function TicketDetailDrawer({
451
+ ticket,
452
+ busy,
453
+ supportSystemDown,
454
+ onSendMessage,
455
+ onClose,
456
+ onReopen,
457
+ onActionCollapsed,
458
+ replyError,
459
+ onClearReplyError
460
+ }) {
461
+ const isClosed = (ticket.status ?? "").toUpperCase() === "CLOSED";
462
+ return /* @__PURE__ */ jsxs3("div", { className: "bg-ods-card border-t border-ods-border px-4 py-4 flex flex-col gap-4", children: [
463
+ /* @__PURE__ */ jsx4(AssignedAgentRow, { assignedOwner: ticket.assignedOwner }),
464
+ ticket.clickup && /* @__PURE__ */ jsx4(TicketLinkedDeliveryCard, { clickup: ticket.clickup }),
465
+ /* @__PURE__ */ jsxs3("div", { children: [
466
+ /* @__PURE__ */ jsx4("p", { className: "text-xs font-medium text-ods-text-secondary mb-2 uppercase tracking-wider", children: "Conversation" }),
467
+ /* @__PURE__ */ jsx4(TicketTimelinePanel, { ticket })
468
+ ] }),
469
+ /* @__PURE__ */ jsxs3("div", { className: "border-t border-ods-border pt-4", children: [
470
+ replyError && /* @__PURE__ */ jsx4(
471
+ ReplyFailureBanner,
472
+ {
473
+ error: replyError,
474
+ onDismiss: onClearReplyError ?? (() => void 0)
475
+ }
476
+ ),
477
+ isClosed ? /* @__PURE__ */ jsx4(
478
+ ReopenAction,
479
+ {
480
+ ticketRef: { id: ticket.id, external_id: ticket.external_id },
481
+ busy,
482
+ supportSystemDown,
483
+ onReopen,
484
+ onActionCollapsed
485
+ }
486
+ ) : /* @__PURE__ */ jsx4(
487
+ TicketReplyComposer,
488
+ {
489
+ ticket,
490
+ busy,
491
+ supportSystemDown,
492
+ onSendMessage,
493
+ onClose
494
+ }
495
+ )
496
+ ] })
497
+ ] });
498
+ }
499
+ var TURN_SEPARATOR_RE = /[\s]{1,16}---[\s]{1,16}/g;
500
+ var TICKET_FEED_FRAME = "bg-ods-card border border-ods-border rounded-[6px] overflow-y-auto w-full";
501
+ var TICKET_FEED_HEIGHT = "h-[60vh] md:h-[420px]";
502
+ var TICKET_FEED_INNER = "flex flex-col gap-4 md:gap-6 px-4 md:px-6 py-4 md:py-6";
503
+ var TICKET_FEED_SKELETON_ROWS = 6;
504
+ function TicketTimelinePanel({ ticket }) {
505
+ const identity = useChatIdentity();
506
+ const externalId = isOptimistic(ticket) ? null : ticket.external_id;
507
+ const { engagements, isLoading } = useTicketEngagements(
508
+ externalId,
509
+ !!externalId,
510
+ TICKET_LIVE_POLL_MS
511
+ );
512
+ const { scrollRef, contentRef } = useStickToBottom({ initial: "instant", resize: "smooth" });
513
+ const bodyTurns = ticket.body ? ticket.body.split(TURN_SEPARATOR_RE).map((t) => t.trim()).filter(Boolean) : [];
514
+ const customerEngagementBodies = new Set(
515
+ engagements.filter((e) => e.authorRole === "customer").map((e) => (e.body ?? "").trim()).filter(Boolean)
516
+ );
517
+ const suppressBodyTurnZero = bodyTurns.length > 0 && customerEngagementBodies.has(bodyTurns[0]);
518
+ const sessionEmailLower = identity.user?.email?.trim().toLowerCase() ?? null;
519
+ const isViewerTheCustomer = !!sessionEmailLower && ticket.customer_emails.some((e) => e.trim().toLowerCase() === sessionEmailLower);
520
+ const viewerName = identity.user?.name?.trim() || null;
521
+ const ticketCustomerName = ticket.customer_name?.trim() || null;
522
+ const customerName = (isViewerTheCustomer ? viewerName : null) || ticketCustomerName || viewerName || identity.user?.email || "You";
523
+ const customerAvatar = isViewerTheCustomer ? identity.user?.avatarUrl ?? void 0 : void 0;
524
+ if (isLoading) {
525
+ 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)) }) });
526
+ }
527
+ if (bodyTurns.length === 0 && engagements.length === 0) {
528
+ return /* @__PURE__ */ jsx4(
529
+ EmptyState,
530
+ {
531
+ type: "generic",
532
+ title: "No conversation yet",
533
+ description: "Reply below to start the thread with the support team.",
534
+ showCTA: false
535
+ }
536
+ );
537
+ }
538
+ return /* @__PURE__ */ jsx4("div", { ref: scrollRef, className: `${TICKET_FEED_FRAME} ${TICKET_FEED_HEIGHT}`, children: /* @__PURE__ */ jsxs3("div", { ref: contentRef, className: TICKET_FEED_INNER, children: [
539
+ bodyTurns.map((turn, i) => {
540
+ if (i === 0 && suppressBodyTurnZero) return null;
541
+ const isResolution = turn.startsWith("[Resolution]");
542
+ const text = isResolution ? turn.replace(/^\[Resolution\]\s*/, "") : turn;
543
+ return /* @__PURE__ */ jsx4(
544
+ ChatMessageRow,
545
+ {
546
+ displayName: customerName,
547
+ avatarUrl: customerAvatar,
548
+ body: text
549
+ },
550
+ `body-${i}-${turn.slice(0, 24)}`
551
+ );
552
+ }),
553
+ engagements.map((eng) => {
554
+ const isCustomer = eng.authorRole === "customer";
555
+ const isOwnReply = isCustomer && !!eng.authorId && !!identity.user?.email && eng.authorId.toLowerCase() === identity.user.email.toLowerCase();
556
+ let author;
557
+ let avatarSrc;
558
+ if (isCustomer && isOwnReply) {
559
+ author = identity.user?.name?.trim() || customerName;
560
+ avatarSrc = identity.user?.avatarUrl ?? void 0;
561
+ } else if (isCustomer) {
562
+ author = ticketCustomerName || "Customer";
563
+ avatarSrc = void 0;
564
+ } else if (eng.authorName && eng.authorAvatarUrl) {
565
+ author = eng.authorName;
566
+ avatarSrc = eng.authorAvatarUrl;
567
+ } else {
568
+ author = "Support team";
569
+ avatarSrc = void 0;
570
+ }
571
+ const engAttachments = mapEngagementAttachments(eng.attachments);
572
+ return /* @__PURE__ */ jsx4(
573
+ ChatMessageRow,
574
+ {
575
+ displayName: author,
576
+ avatarUrl: avatarSrc,
577
+ timeLabel: eng.createdAt ? formatRelativeTime(eng.createdAt) : null,
578
+ body: stripAttachmentsPreamble(eng.body ?? ""),
579
+ footer: engAttachments.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "mt-2", children: /* @__PURE__ */ jsx4(TicketAttachmentsList, { attachments: engAttachments, size: "compact" }) }) : null
580
+ },
581
+ eng.id
582
+ );
583
+ })
584
+ ] }) });
585
+ }
586
+ function mapEngagementAttachments(files) {
587
+ return files.map((f) => ({
588
+ id: f.id,
589
+ fileName: f.name ?? `file-${f.id}`,
590
+ fileSize: f.size ? formatBytes(f.size) : "",
591
+ // Show an inline thumbnail for image attachments (the signed `url` is a
592
+ // viewable URL). Non-images fall back to the file-type icon. SquareAvatar
593
+ // degrades to initials on a broken/expired image URL.
594
+ thumbnailSrc: f.url && (f.mime?.startsWith("image/") ?? false) ? f.url : void 0,
595
+ onDownload: f.url ? () => window.open(f.url, "_blank", "noopener,noreferrer") : void 0
596
+ }));
597
+ }
598
+ function formatBytes(size) {
599
+ if (size < 1024) return `${size} B`;
600
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
601
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
602
+ }
603
+ var ATTACHMENTS_PREAMBLE_RE = /\s*\n\s*Attachments\b[^]*$/i;
604
+ function stripAttachmentsPreamble(body) {
605
+ return body.replace(ATTACHMENTS_PREAMBLE_RE, "").trim();
606
+ }
607
+ function ReopenAction({
608
+ ticketRef,
609
+ busy,
610
+ supportSystemDown,
611
+ onReopen,
612
+ onActionCollapsed
613
+ }) {
614
+ const handleReopen = async () => {
615
+ void await onReopen(ticketRef);
616
+ };
617
+ return /* @__PURE__ */ jsx4("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx4(
618
+ Button,
619
+ {
620
+ type: "button",
621
+ variant: "outline",
622
+ size: "small",
623
+ onClick: () => void handleReopen(),
624
+ disabled: busy || supportSystemDown,
625
+ loading: busy,
626
+ children: "Reopen"
627
+ }
628
+ ) });
629
+ }
630
+ function ReplyFailureBanner({
631
+ error,
632
+ onDismiss
633
+ }) {
634
+ return /* @__PURE__ */ jsxs3(
635
+ "div",
636
+ {
637
+ role: "status",
638
+ "aria-live": "polite",
639
+ 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",
640
+ children: [
641
+ /* @__PURE__ */ jsx4("span", { className: "font-medium leading-snug", children: error.message }),
642
+ /* @__PURE__ */ jsx4(
643
+ Button,
644
+ {
645
+ type: "button",
646
+ variant: "transparent",
647
+ onClick: onDismiss,
648
+ "aria-label": "Dismiss reply failure",
649
+ className: "ml-auto px-2 py-0.5 text-xs font-medium uppercase tracking-wider text-ods-attention-red-error hover:bg-ods-attention-red-error/10 border-transparent",
650
+ children: "Dismiss"
651
+ }
652
+ )
653
+ ]
654
+ }
655
+ );
656
+ }
657
+ function AssignedAgentRow({
658
+ assignedOwner
659
+ }) {
660
+ const trimmedName = assignedOwner?.name?.trim() || null;
661
+ const emailFallback = assignedOwner?.email?.trim() || null;
662
+ const displayLabel = trimmedName ?? (emailFallback ? emailFallback.split("@")[0] : null);
663
+ return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-xs", children: [
664
+ /* @__PURE__ */ jsx4("span", { className: "text-ods-text-secondary uppercase tracking-wider font-medium", children: "Assigned to" }),
665
+ displayLabel ? /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-1.5 text-ods-text-primary font-medium", children: [
666
+ /* @__PURE__ */ jsx4(
667
+ SquareAvatar,
668
+ {
669
+ size: "sm",
670
+ variant: "round",
671
+ src: assignedOwner?.avatarUrl ?? void 0,
672
+ alt: displayLabel,
673
+ fallback: displayLabel
674
+ }
675
+ ),
676
+ displayLabel
677
+ ] }) : /* @__PURE__ */ jsx4("span", { className: "text-ods-text-secondary italic", children: "Unassigned" })
678
+ ] });
679
+ }
680
+
681
+ // src/components/tickets/ticket-row.tsx
682
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
683
+ function TicketRow({
684
+ ticket,
685
+ expanded,
686
+ onToggle,
687
+ busy,
688
+ supportSystemDown,
689
+ onSendMessage,
690
+ onClose,
691
+ onReopen,
692
+ onActionCollapsed,
693
+ id
694
+ }) {
695
+ const optimistic = isOptimistic(ticket);
696
+ const rowRef = useRef(null);
697
+ const handleClick = useCallback2(() => {
698
+ onToggle(ticket.id);
699
+ scrollElementIntoView(rowRef.current, {
700
+ adjustTargetY: (raw) => {
701
+ if (!rowRef.current) return raw;
702
+ const expandedDrawer = document.querySelector(
703
+ 'div[id^="ticket-drawer-"]'
704
+ );
705
+ if (!(expandedDrawer instanceof HTMLElement)) return raw;
706
+ const drawerRect = expandedDrawer.getBoundingClientRect();
707
+ const myRect = rowRef.current.getBoundingClientRect();
708
+ if (drawerRect.bottom > myRect.top) return raw;
709
+ return raw - drawerRect.height;
710
+ }
711
+ });
712
+ }, [onToggle, ticket.id]);
713
+ const tileData = {
714
+ id: ticket.id,
715
+ title: ticket.subject ?? "(untitled)",
716
+ ticketNumber: `#${ticket.external_id}`,
717
+ status: ticket.status ?? "OPEN",
718
+ // Surface the HubSpot pipeline stage label ("New" / "Closed" /
719
+ // "Waiting on contact" / "Waiting on version release") instead of
720
+ // the canonical "Active"/"Resolved" default. The variant + check
721
+ // icon still come from `status` (CLOSED → check; OPEN → no check),
722
+ // so the badge accurately reflects "Closed" with a checkmark.
723
+ statusLabel: ticket.pipeline_stage_label ?? void 0,
724
+ category: ticket.customer_company ?? void 0,
725
+ timeAgo: ticket.hubspot_updated_at ? formatRelativeTime(ticket.hubspot_updated_at) : void 0,
726
+ // Linked-work chip: surfaced whenever the ticket has a linked
727
+ // ClickUp task. Uses the linked task's own status so the chip text
728
+ // reads "Working" / "Waiting on version release" / etc. — useful
729
+ // signal pre-expand. Falls back to a generic "Linked work" label
730
+ // when the task exists but its status hasn't synced yet.
731
+ linkedTaskLabel: ticket.clickup ? ticket.clickup.status ? ticket.clickup.status.replace(/\b\w/g, (c) => c.toUpperCase()) : "Linked work" : void 0
732
+ };
733
+ return /* @__PURE__ */ jsx5("div", { ref: rowRef, id, className: "scroll-mt-24", children: /* @__PURE__ */ jsxs4(
734
+ Collapsible,
735
+ {
736
+ open: expanded && !optimistic,
737
+ className: "border-b border-ods-border last:border-b-0",
738
+ children: [
739
+ /* @__PURE__ */ jsx5(
740
+ ChatTicketItem,
741
+ {
742
+ ticket: tileData,
743
+ onClick: optimistic ? void 0 : handleClick,
744
+ "aria-expanded": expanded && !optimistic,
745
+ "aria-controls": `ticket-drawer-${ticket.id}`
746
+ }
747
+ ),
748
+ /* @__PURE__ */ jsx5(
749
+ CollapsibleContent2,
750
+ {
751
+ id: `ticket-drawer-${ticket.id}`,
752
+ className: "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
753
+ children: /* @__PURE__ */ jsx5(
754
+ TicketDetailDrawer,
755
+ {
756
+ ticket,
757
+ busy,
758
+ supportSystemDown,
759
+ onSendMessage,
760
+ onClose,
761
+ onReopen,
762
+ onActionCollapsed
763
+ }
764
+ )
765
+ }
766
+ )
767
+ ]
768
+ }
769
+ ) });
770
+ }
771
+
772
+ // src/components/tickets/hooks/use-tickets-list.ts
773
+ import { useQuery as useQuery2 } from "@tanstack/react-query";
774
+ var FIND_TICKET_ENDPOINT = "/api/chat/agent/find-ticket";
775
+ var DEFAULT_PAGE_SIZE = 20;
776
+ function useTicketsList(filters) {
777
+ const findTicketEndpoint = useRequiredChatRuntime().endpoints.findTicketUrl ?? FIND_TICKET_ENDPOINT;
778
+ const customerEmail = filters.customerEmail;
779
+ const search = (filters.search ?? "").trim();
780
+ const status = (filters.status ?? "").trim().toLowerCase();
781
+ const statusFilter = status && status !== "all" ? status : "";
782
+ const page = Math.max(1, Math.floor(filters.page ?? 1) || 1);
783
+ const pageSize = Math.max(1, Math.min(100, Math.floor(filters.pageSize ?? DEFAULT_PAGE_SIZE) || DEFAULT_PAGE_SIZE));
784
+ const enabled = !!customerEmail;
785
+ const identityKey = customerEmail || "anon";
786
+ const refetchInterval = filters.refetchInterval ?? false;
787
+ const query = useQuery2({
788
+ queryKey: ["tickets", "self", identityKey, search, statusFilter, page, pageSize],
789
+ enabled,
790
+ // Caches OFF — every mount + every focus + every navigation triggers
791
+ // a fresh fetch. The ticket data is the customer's own list of
792
+ // tickets they expect to be live (sync agents reply, statuses flip,
793
+ // new comments arrive); a stale window of any size is worse than
794
+ // a sub-second refetch.
795
+ staleTime: 0,
796
+ gcTime: 0,
797
+ refetchOnMount: "always",
798
+ refetchOnWindowFocus: true,
799
+ // Live status: poll while the caller opts in (drawer open). Defaults to
800
+ // false. `refetchIntervalInBackground` stays false (the default) so polling
801
+ // pauses on a hidden tab — no wasted requests when the user tabs away.
802
+ refetchInterval,
803
+ queryFn: async () => {
804
+ const body = {
805
+ query: search,
806
+ page,
807
+ pageSize
808
+ };
809
+ if (statusFilter) body.status = statusFilter;
810
+ const response = await embedAuthedFetch(findTicketEndpoint, {
811
+ method: "POST",
812
+ body: JSON.stringify(body)
813
+ });
814
+ if (!response.ok) {
815
+ const text = await response.text().catch(() => "");
816
+ throw new Error(`find-ticket failed: ${response.status} ${text.slice(0, 200)}`);
817
+ }
818
+ return await response.json();
819
+ }
820
+ });
821
+ const data = query.data;
822
+ const totalCount = data?.totalCount ?? data?.count ?? (data?.tickets?.length ?? 0);
823
+ const echoedPage = data?.page ?? page;
824
+ const echoedPageSize = data?.pageSize ?? pageSize;
825
+ const totalPages = data?.totalPages ?? Math.max(1, Math.ceil(totalCount / echoedPageSize));
826
+ return {
827
+ tickets: data?.tickets ?? [],
828
+ // Loading-state-truth = `data === undefined`. TanStack v5's
829
+ // `isPending` / `isLoading` flags can be `false` in transient
830
+ // windows where the query is enabled-but-fetch-not-yet-fired
831
+ // OR where stale-data exists from a sibling cache slot — both
832
+ // produced the EmptyState flash on /tickets first load. Treating
833
+ // "no data for THIS query slot yet" as the universal loading
834
+ // signal can't lie:
835
+ // - Initial render after enabled flips: data === undefined → load
836
+ // - Background refetch with existing data: data !== undefined → no load
837
+ // - Filter-change refetch landing on empty results: data?.tickets===[]
838
+ // + isFetching → bridge skeleton (the `||` branch)
839
+ // Loading-state-truth = `data === undefined`. TanStack v5's
840
+ // `isPending` / `isLoading` flags can be `false` in transient
841
+ // windows where the query is enabled-but-fetch-not-yet-fired
842
+ // OR where stale-data exists from a sibling cache slot. Treating
843
+ // "no data for THIS query slot yet" as the universal loading
844
+ // signal can't lie:
845
+ // - Initial render after enabled flips: data === undefined → load
846
+ // - Background refetch with existing data: data !== undefined → no load
847
+ // - Filter-change refetch landing on empty results: data?.tickets===[]
848
+ // + isFetching → bridge skeleton (the `||` branch)
849
+ isLoading: enabled && (data === void 0 || query.isFetching && (data?.tickets ?? []).length === 0),
850
+ isFetching: query.isFetching,
851
+ error: query.error ?? null,
852
+ refetch: () => {
853
+ void query.refetch();
854
+ },
855
+ lastUpdatedAt: query.dataUpdatedAt || null,
856
+ totalCount,
857
+ page: echoedPage,
858
+ pageSize: echoedPageSize,
859
+ totalPages
860
+ };
861
+ }
862
+
863
+ // src/components/tickets/hooks/use-ticket-actions.ts
864
+ import { useCallback as useCallback3, useEffect, useMemo, useRef as useRef2, useState as useState3 } from "react";
865
+ import { useQueryClient } from "@tanstack/react-query";
866
+ var TICKET_ACTION_ENDPOINT = "/api/chat/agent/ticket-action";
867
+ var REPLY_BANNER_CODES = /* @__PURE__ */ new Set([
868
+ "HUBSPOT_5XX",
869
+ "HUBSPOT_400_VALIDATION",
870
+ "HUBSPOT_404_THREAD",
871
+ "HUBSPOT_REPLY_UNKNOWN"
872
+ ]);
873
+ var MIRROR_SYNC_BACKOFF_MS = [3e3, 6e3, 12e3];
874
+ function useTicketActions(options) {
875
+ const queryClient = useQueryClient();
876
+ const ticketActionEndpoint = useRequiredChatRuntime().endpoints.ticketActionUrl ?? TICKET_ACTION_ENDPOINT;
877
+ const { prependOptimistic, removeOptimistic, removeTicketFromCache, toast: toast2, onSupportSystemDown } = options;
878
+ const formInFlightRef = useRef2(false);
879
+ const [isSubmittingForm, setIsSubmittingForm] = useState3(false);
880
+ const busyRowsRef = useRef2(/* @__PURE__ */ new Set());
881
+ const [busyRows, setBusyRows] = useState3(() => /* @__PURE__ */ new Set());
882
+ const setRowBusy = useCallback3((id, busy) => {
883
+ if (busy) busyRowsRef.current.add(id);
884
+ else busyRowsRef.current.delete(id);
885
+ setBusyRows(new Set(busyRowsRef.current));
886
+ }, []);
887
+ const isRowBusy = useCallback3((id) => busyRows.has(id), [busyRows]);
888
+ const [replyErrorByTicket, setReplyErrorByTicket] = useState3(() => /* @__PURE__ */ new Map());
889
+ const setReplyError = useCallback3(
890
+ (externalId, mapped) => {
891
+ setReplyErrorByTicket((prev) => {
892
+ const next = new Map(prev);
893
+ if (mapped) next.set(externalId, mapped);
894
+ else next.delete(externalId);
895
+ return next;
896
+ });
897
+ },
898
+ []
899
+ );
900
+ const replyErrorFor = useCallback3(
901
+ (externalId) => replyErrorByTicket.get(externalId) ?? null,
902
+ [replyErrorByTicket]
903
+ );
904
+ const clearReplyError = useCallback3(
905
+ (externalId) => setReplyError(externalId, null),
906
+ [setReplyError]
907
+ );
908
+ const watcherControllersRef = useRef2(/* @__PURE__ */ new Map());
909
+ useEffect(() => {
910
+ return () => {
911
+ for (const controller of watcherControllersRef.current.values()) {
912
+ controller.abort();
913
+ }
914
+ watcherControllersRef.current.clear();
915
+ };
916
+ }, []);
917
+ const queueRef = useRef2(Promise.resolve());
918
+ const enqueue = useCallback3((work) => {
919
+ const next = queueRef.current.then(work, work);
920
+ queueRef.current = next.catch(() => void 0);
921
+ return next;
922
+ }, []);
923
+ const executeTicketAction = useCallback3(
924
+ async (toolName, args) => {
925
+ const res = await embedAuthedFetch(ticketActionEndpoint, {
926
+ method: "POST",
927
+ body: JSON.stringify({ tool_name: toolName, args })
928
+ });
929
+ const body = await res.json().catch(() => ({}));
930
+ if (!res.ok) {
931
+ const code = resolveErrorCode(body.code, res.status);
932
+ const message = body.error || `${toolName} failed (${res.status})`;
933
+ throw new TicketActionFailure(code, message, res);
934
+ }
935
+ return body;
936
+ },
937
+ [ticketActionEndpoint]
938
+ );
939
+ const watchMirrorSync = useCallback3(
940
+ (placeholderId, expectedTicketId) => {
941
+ const prior = watcherControllersRef.current.get(placeholderId);
942
+ if (prior) prior.abort();
943
+ const controller = new AbortController();
944
+ watcherControllersRef.current.set(placeholderId, controller);
945
+ const schedule = async () => {
946
+ try {
947
+ for (let i = 0; i < MIRROR_SYNC_BACKOFF_MS.length; i++) {
948
+ if (controller.signal.aborted) return;
949
+ await new Promise((resolve) => {
950
+ const t = setTimeout(resolve, MIRROR_SYNC_BACKOFF_MS[i]);
951
+ controller.signal.addEventListener(
952
+ "abort",
953
+ () => {
954
+ clearTimeout(t);
955
+ resolve();
956
+ },
957
+ { once: true }
958
+ );
959
+ });
960
+ if (controller.signal.aborted) return;
961
+ await queryClient.invalidateQueries({ queryKey: ["tickets"] });
962
+ if (expectedTicketId && cacheContainsTicket(queryClient, expectedTicketId)) {
963
+ removeOptimistic(placeholderId);
964
+ return;
965
+ }
966
+ }
967
+ if (!controller.signal.aborted) {
968
+ removeOptimistic(placeholderId);
969
+ toast2({
970
+ title: "Couldn't confirm ticket",
971
+ description: "If the ticket doesn't appear shortly, please contact support.",
972
+ variant: "destructive"
973
+ });
974
+ }
975
+ } finally {
976
+ if (watcherControllersRef.current.get(placeholderId) === controller) {
977
+ watcherControllersRef.current.delete(placeholderId);
978
+ }
979
+ }
980
+ };
981
+ void schedule();
982
+ },
983
+ [queryClient, removeOptimistic, toast2]
984
+ );
985
+ const lastUpdateErrorRef = useRef2(null);
986
+ const surfaceError = useCallback3(
987
+ (err, action) => {
988
+ const mapped = mapTicketActionError(err);
989
+ lastUpdateErrorRef.current = mapped;
990
+ if (mapped.supportSystemDown) onSupportSystemDown();
991
+ toast2({
992
+ title: `Could not ${action}`,
993
+ description: mapped.message,
994
+ variant: "destructive"
995
+ });
996
+ return mapped;
997
+ },
998
+ [toast2, onSupportSystemDown]
999
+ );
1000
+ const submitTicket = useCallback3(
1001
+ async (input) => {
1002
+ if (formInFlightRef.current) return false;
1003
+ formInFlightRef.current = true;
1004
+ setIsSubmittingForm(true);
1005
+ const placeholderId = `temp-${cryptoRandomId()}`;
1006
+ const placeholder = {
1007
+ id: placeholderId,
1008
+ external_id: "Pending sync\u2026",
1009
+ subject: input.subject.trim(),
1010
+ preview: input.content.trim().slice(0, 400),
1011
+ body: input.content.trim(),
1012
+ status: "OPEN",
1013
+ pipeline_stage_label: "New",
1014
+ clickup_task_id: null,
1015
+ clickup: null,
1016
+ priority: null,
1017
+ customer_emails: [],
1018
+ customer_company: null,
1019
+ // Optimistic placeholder has no resolved HubSpot contact yet
1020
+ // — the real ticket row replaces this within a couple of
1021
+ // seconds via the mirror refetch. Drawer uses live chat
1022
+ // identity for own-replies during this window anyway.
1023
+ customer_name: null,
1024
+ // No assignee until the real ticket lands. Drawer renders
1025
+ // "Unassigned" for this brief window.
1026
+ assigned_to: null,
1027
+ assignedOwner: null,
1028
+ hubspot_updated_at: (/* @__PURE__ */ new Date()).toISOString(),
1029
+ _optimistic: true
1030
+ };
1031
+ prependOptimistic(placeholder);
1032
+ try {
1033
+ return await enqueue(async () => {
1034
+ const result = await executeTicketAction("create_ticket", {
1035
+ subject: input.subject.trim(),
1036
+ content: input.content.trim(),
1037
+ ...input.attachments?.length ? { attachments: input.attachments } : {}
1038
+ });
1039
+ if (result.mirror_synced === false) {
1040
+ toast2(TOAST_COPY.open_mirror_pending);
1041
+ watchMirrorSync(placeholderId, result.ticket_id);
1042
+ } else {
1043
+ toast2(TOAST_COPY.open_success);
1044
+ await queryClient.invalidateQueries({ queryKey: ["tickets"] });
1045
+ removeOptimistic(placeholderId);
1046
+ }
1047
+ return true;
1048
+ });
1049
+ } catch (err) {
1050
+ removeOptimistic(placeholderId);
1051
+ surfaceError(err, "open ticket");
1052
+ return false;
1053
+ } finally {
1054
+ formInFlightRef.current = false;
1055
+ setIsSubmittingForm(false);
1056
+ }
1057
+ },
1058
+ [
1059
+ enqueue,
1060
+ executeTicketAction,
1061
+ prependOptimistic,
1062
+ removeOptimistic,
1063
+ queryClient,
1064
+ toast2,
1065
+ watchMirrorSync,
1066
+ surfaceError
1067
+ ]
1068
+ );
1069
+ const updateTicket = useCallback3(
1070
+ async (ticket, serverArgs, successCopy, action) => {
1071
+ if (busyRowsRef.current.has(ticket.id)) return false;
1072
+ setRowBusy(ticket.id, true);
1073
+ try {
1074
+ return await enqueue(async () => {
1075
+ await executeTicketAction("update_ticket", {
1076
+ ...serverArgs,
1077
+ ticket_id: ticket.external_id
1078
+ });
1079
+ toast2(successCopy);
1080
+ const statusUpdate = serverArgs.status ?? null;
1081
+ if (statusUpdate) {
1082
+ queryClient.setQueriesData(
1083
+ { queryKey: ["tickets"] },
1084
+ (prev) => {
1085
+ if (!prev || !Array.isArray(prev.tickets)) return prev;
1086
+ let mutated = false;
1087
+ const nextTickets = prev.tickets.map((t) => {
1088
+ if (t.id !== ticket.id || t.status === statusUpdate) return t;
1089
+ mutated = true;
1090
+ return { ...t, status: statusUpdate };
1091
+ });
1092
+ return mutated ? { ...prev, tickets: nextTickets } : prev;
1093
+ }
1094
+ );
1095
+ }
1096
+ await queryClient.invalidateQueries({ queryKey: ["ticket-engagements"] });
1097
+ return true;
1098
+ });
1099
+ } catch (err) {
1100
+ const mapped = surfaceError(err, action);
1101
+ if (mapped.removeRowFromCache) {
1102
+ removeTicketFromCache(ticket.id);
1103
+ }
1104
+ return false;
1105
+ } finally {
1106
+ setRowBusy(ticket.id, false);
1107
+ }
1108
+ },
1109
+ // `busyRowsRef` is read via .current — needs no dep entry. `busyRows`
1110
+ // state isn't read inside this callback (only by `isRowBusy` selector
1111
+ // outside), so listing it would churn the closure on every flag flip
1112
+ // and cascade-recreate addNote/closeTicket/etc.
1113
+ [setRowBusy, enqueue, executeTicketAction, queryClient, toast2, surfaceError, removeTicketFromCache]
1114
+ );
1115
+ const sendMessage = useCallback3(
1116
+ async (ticket, text, attachments) => {
1117
+ const trimmed = text.trim();
1118
+ const hasText = trimmed.length > 0;
1119
+ const hasFiles = attachments.length > 0;
1120
+ if (!hasText && !hasFiles) return false;
1121
+ lastUpdateErrorRef.current = null;
1122
+ const ok = await updateTicket(
1123
+ ticket,
1124
+ {
1125
+ ...hasText ? { content_addendum: trimmed } : {},
1126
+ ...hasFiles ? { attachments } : {}
1127
+ },
1128
+ TOAST_COPY.comment_success,
1129
+ "send message"
1130
+ );
1131
+ if (ok) {
1132
+ clearReplyError(ticket.external_id);
1133
+ } else {
1134
+ const mapped = lastUpdateErrorRef.current;
1135
+ if (mapped && REPLY_BANNER_CODES.has(mapped.code)) {
1136
+ setReplyError(ticket.external_id, mapped);
1137
+ }
1138
+ lastUpdateErrorRef.current = null;
1139
+ }
1140
+ return ok;
1141
+ },
1142
+ [updateTicket, clearReplyError, setReplyError]
1143
+ );
1144
+ const closeTicket = useCallback3(
1145
+ (ticket, resolution) => updateTicket(
1146
+ ticket,
1147
+ {
1148
+ status: "CLOSED",
1149
+ ...resolution?.trim() ? { resolution: resolution.trim() } : {}
1150
+ },
1151
+ TOAST_COPY.close_success,
1152
+ "close ticket"
1153
+ ),
1154
+ [updateTicket]
1155
+ );
1156
+ const reopenTicket = useCallback3(
1157
+ (ticket) => updateTicket(ticket, { status: "OPEN" }, TOAST_COPY.reopen_success, "reopen ticket"),
1158
+ [updateTicket]
1159
+ );
1160
+ return useMemo(
1161
+ () => ({
1162
+ submitTicket,
1163
+ sendMessage,
1164
+ closeTicket,
1165
+ reopenTicket,
1166
+ isSubmittingForm,
1167
+ isRowBusy,
1168
+ replyErrorFor,
1169
+ clearReplyError
1170
+ }),
1171
+ [
1172
+ submitTicket,
1173
+ sendMessage,
1174
+ closeTicket,
1175
+ reopenTicket,
1176
+ isSubmittingForm,
1177
+ isRowBusy,
1178
+ replyErrorFor,
1179
+ clearReplyError
1180
+ ]
1181
+ );
1182
+ }
1183
+ var TicketActionFailure = class extends Error {
1184
+ constructor(code, message, response) {
1185
+ super(message);
1186
+ this.code = code;
1187
+ this.response = response;
1188
+ }
1189
+ };
1190
+ function mapTicketActionError(err) {
1191
+ if (err instanceof TicketActionFailure) {
1192
+ switch (err.code) {
1193
+ case "PROPOSAL_NOT_CLAIMABLE":
1194
+ return {
1195
+ code: err.code,
1196
+ message: "This action was already processed.",
1197
+ supportSystemDown: false,
1198
+ removeRowFromCache: false
1199
+ };
1200
+ case "TICKET_NOT_FOUND":
1201
+ return {
1202
+ code: err.code,
1203
+ message: "This ticket is no longer available.",
1204
+ supportSystemDown: false,
1205
+ removeRowFromCache: true
1206
+ };
1207
+ case "TICKET_OWNERSHIP_DENIED":
1208
+ return {
1209
+ code: err.code,
1210
+ message: "You can only act on tickets you opened.",
1211
+ supportSystemDown: false,
1212
+ removeRowFromCache: false
1213
+ };
1214
+ case "HUBSPOT_DISCONNECTED":
1215
+ return {
1216
+ code: err.code,
1217
+ message: "Support system temporarily unavailable.",
1218
+ supportSystemDown: true,
1219
+ removeRowFromCache: false
1220
+ };
1221
+ case "RATE_LIMITED": {
1222
+ const retryAfterRaw = err.response?.headers.get("Retry-After");
1223
+ const retryAfterSeconds = retryAfterRaw ? parseInt(retryAfterRaw, 10) : void 0;
1224
+ return {
1225
+ code: err.code,
1226
+ message: retryAfterSeconds ? `Too many actions. Try again in ${retryAfterSeconds}s.` : "Too many actions. Try again shortly.",
1227
+ supportSystemDown: false,
1228
+ removeRowFromCache: false,
1229
+ ...retryAfterSeconds ? { retryAfterSeconds } : {}
1230
+ };
1231
+ }
1232
+ case "INVALID_TOOL_ARGS":
1233
+ return {
1234
+ code: err.code,
1235
+ message: "Your input was rejected. Please review and try again.",
1236
+ supportSystemDown: false,
1237
+ removeRowFromCache: false
1238
+ };
1239
+ case "HUBSPOT_5XX":
1240
+ return {
1241
+ code: err.code,
1242
+ message: "We couldn't reach the support system. Your reply wasn't sent \u2014 please retry in a moment.",
1243
+ supportSystemDown: false,
1244
+ removeRowFromCache: false
1245
+ };
1246
+ case "HUBSPOT_400_VALIDATION":
1247
+ return {
1248
+ code: err.code,
1249
+ message: "Your reply was rejected. Please rephrase or remove unsupported content and try again.",
1250
+ supportSystemDown: false,
1251
+ removeRowFromCache: false
1252
+ };
1253
+ case "HUBSPOT_404_THREAD":
1254
+ return {
1255
+ code: err.code,
1256
+ message: "This conversation is no longer accepting replies. Open a new ticket to continue.",
1257
+ supportSystemDown: false,
1258
+ removeRowFromCache: false
1259
+ };
1260
+ case "HUBSPOT_REPLY_UNKNOWN":
1261
+ return {
1262
+ code: err.code,
1263
+ message: "Your reply didn't go through. Please retry.",
1264
+ supportSystemDown: false,
1265
+ removeRowFromCache: false
1266
+ };
1267
+ default:
1268
+ return {
1269
+ code: "UNKNOWN",
1270
+ message: err.message || "Something went wrong. Please try again.",
1271
+ supportSystemDown: false,
1272
+ removeRowFromCache: false
1273
+ };
1274
+ }
1275
+ }
1276
+ return {
1277
+ code: "UNKNOWN",
1278
+ message: err instanceof Error ? err.message : "Something went wrong. Please try again.",
1279
+ supportSystemDown: false,
1280
+ removeRowFromCache: false
1281
+ };
1282
+ }
1283
+ function cryptoRandomId() {
1284
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
1285
+ return crypto.randomUUID();
1286
+ }
1287
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1288
+ }
1289
+ function cacheContainsTicket(queryClient, expectedTicketId) {
1290
+ const entries = queryClient.getQueriesData({
1291
+ queryKey: ["tickets"]
1292
+ });
1293
+ for (const [, data] of entries) {
1294
+ if (data && Array.isArray(data.tickets) && data.tickets.some((t) => t.external_id === expectedTicketId)) {
1295
+ return true;
1296
+ }
1297
+ }
1298
+ return false;
1299
+ }
1300
+ function resolveErrorCode(bodyCode, status) {
1301
+ if (bodyCode) return bodyCode;
1302
+ if (status === 429) return "RATE_LIMITED";
1303
+ if (status === 412) return "HUBSPOT_DISCONNECTED";
1304
+ return "UNKNOWN";
1305
+ }
1306
+
1307
+ // src/components/tickets/ticket-center.tsx
1308
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1309
+ function TicketCenter({ toast: toast2 = toast } = {}) {
1310
+ const identity = useChatIdentity();
1311
+ if (identity.isLoading) {
1312
+ return /* @__PURE__ */ jsx6(TicketCenterSkeleton, {});
1313
+ }
1314
+ if (identity.authTier === "anon" || !identity.user?.email) {
1315
+ return /* @__PURE__ */ jsx6(
1316
+ EmptyState,
1317
+ {
1318
+ type: "generic",
1319
+ title: "Sign in to manage tickets",
1320
+ description: "View, open, and follow up on support tickets after signing in.",
1321
+ showCTA: false
1322
+ }
1323
+ );
1324
+ }
1325
+ return /* @__PURE__ */ jsx6(
1326
+ TicketCenterAuthed,
1327
+ {
1328
+ toast: toast2,
1329
+ sessionEmail: identity.user.email
1330
+ }
1331
+ );
1332
+ }
1333
+ function TicketCenterAuthed({
1334
+ toast: toast2,
1335
+ sessionEmail
1336
+ }) {
1337
+ const queryClient = useQueryClient2();
1338
+ const { tickets, isLoading, isFetching, refetch, lastUpdatedAt } = useTicketsList({
1339
+ customerEmail: sessionEmail
1340
+ });
1341
+ const [optimisticTickets, setOptimisticTickets] = useState4([]);
1342
+ const [expandedTicketId, setExpandedTicketId] = useState4(null);
1343
+ const [supportSystemDown, setSupportSystemDown] = useState4(false);
1344
+ const prependOptimistic = useCallback4((placeholder) => {
1345
+ setOptimisticTickets((prev) => [placeholder, ...prev]);
1346
+ }, []);
1347
+ const removeOptimistic = useCallback4((placeholderId) => {
1348
+ setOptimisticTickets((prev) => prev.filter((t) => t.id !== placeholderId));
1349
+ setExpandedTicketId((prev) => prev === placeholderId ? null : prev);
1350
+ }, []);
1351
+ const removeTicketFromCache = useCallback4(
1352
+ (ticketId) => {
1353
+ queryClient.setQueriesData(
1354
+ { queryKey: ["tickets"] },
1355
+ (prev) => (prev ?? []).filter((t) => t.id !== ticketId)
1356
+ );
1357
+ setExpandedTicketId((prev) => prev === ticketId ? null : prev);
1358
+ },
1359
+ [queryClient]
1360
+ );
1361
+ const actions = useTicketActions({
1362
+ prependOptimistic,
1363
+ removeOptimistic,
1364
+ removeTicketFromCache,
1365
+ toast: toast2,
1366
+ onSupportSystemDown: () => setSupportSystemDown(true)
1367
+ });
1368
+ const toggleRow = useCallback4((id) => {
1369
+ setExpandedTicketId((prev) => prev === id ? null : id);
1370
+ }, []);
1371
+ const merged = [...optimisticTickets, ...tickets];
1372
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-6", children: [
1373
+ /* @__PURE__ */ jsx6(
1374
+ TicketOpenForm,
1375
+ {
1376
+ onSubmit: (input) => actions.submitTicket(input),
1377
+ isSubmitting: actions.isSubmittingForm,
1378
+ supportSystemDown
1379
+ }
1380
+ ),
1381
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
1382
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between gap-3", children: [
1383
+ /* @__PURE__ */ jsx6("p", { className: "text-xs font-medium text-ods-text-secondary uppercase tracking-wider", children: "Your Current Tickets" }),
1384
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-3 text-xs text-ods-text-secondary", children: [
1385
+ lastUpdatedAt && /* @__PURE__ */ jsxs5("span", { children: [
1386
+ "Updated ",
1387
+ formatRelativeTime(new Date(lastUpdatedAt))
1388
+ ] }),
1389
+ /* @__PURE__ */ jsx6(
1390
+ Button,
1391
+ {
1392
+ type: "button",
1393
+ variant: "transparent",
1394
+ size: "small",
1395
+ onClick: refetch,
1396
+ disabled: isFetching,
1397
+ "aria-label": "Refresh ticket list",
1398
+ leftIcon: /* @__PURE__ */ jsx6(RefreshCw, { className: "h-4 w-4" })
1399
+ }
1400
+ )
1401
+ ] })
1402
+ ] }),
1403
+ isLoading ? /* @__PURE__ */ jsx6(TicketListSkeleton, {}) : merged.length === 0 ? /* @__PURE__ */ jsx6(Card, { className: "p-6", children: /* @__PURE__ */ jsx6(
1404
+ EmptyState,
1405
+ {
1406
+ type: "generic",
1407
+ title: "No tickets yet",
1408
+ description: "Open one above to start the conversation.",
1409
+ showCTA: false
1410
+ }
1411
+ ) }) : /* @__PURE__ */ jsx6(Card, { className: "overflow-hidden", children: merged.map((ticket) => /* @__PURE__ */ jsx6(
1412
+ TicketRow,
1413
+ {
1414
+ id: devSectionAnchorId("ticket", ticket.external_id),
1415
+ ticket,
1416
+ expanded: expandedTicketId === ticket.id,
1417
+ onToggle: toggleRow,
1418
+ busy: isOptimistic(ticket) ? false : actions.isRowBusy(ticket.id),
1419
+ supportSystemDown,
1420
+ onSendMessage: actions.sendMessage,
1421
+ onClose: actions.closeTicket,
1422
+ onReopen: actions.reopenTicket,
1423
+ onActionCollapsed: () => setExpandedTicketId(null)
1424
+ },
1425
+ ticket.id
1426
+ )) })
1427
+ ] })
1428
+ ] });
1429
+ }
1430
+ function TicketCenterSkeleton() {
1431
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-6", children: [
1432
+ /* @__PURE__ */ jsxs5(Card, { className: "p-6", children: [
1433
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-7 w-48 mb-4" }),
1434
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-10 w-full mb-3" }),
1435
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-24 w-full" })
1436
+ ] }),
1437
+ /* @__PURE__ */ jsx6(TicketListSkeleton, {})
1438
+ ] });
1439
+ }
1440
+ function TicketListSkeleton() {
1441
+ 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: [
1442
+ /* @__PURE__ */ jsxs5("div", { className: "flex-1 flex flex-col gap-2", children: [
1443
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-4 w-2/3" }),
1444
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-3 w-full" })
1445
+ ] }),
1446
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-8 w-20" }),
1447
+ /* @__PURE__ */ jsx6(Skeleton, { className: "h-8 w-16" })
1448
+ ] }, i)) });
1449
+ }
1450
+
1451
+ // src/components/tickets/help-center-list.tsx
1452
+ import { useCallback as useCallback6, useState as useState6 } from "react";
1453
+ import { useQueryClient as useQueryClient3 } from "@tanstack/react-query";
1454
+ init_unified_pagination();
1455
+
1456
+ // src/components/tickets/help-center-card.tsx
1457
+ import { useCallback as useCallback5, useEffect as useEffect2, useRef as useRef3 } from "react";
1458
+ import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1459
+ function HelpCenterCard({
1460
+ ticket,
1461
+ expanded,
1462
+ onToggle,
1463
+ busy,
1464
+ supportSystemDown,
1465
+ onSendMessage,
1466
+ onClose,
1467
+ onReopen,
1468
+ onActionCollapsed,
1469
+ replyError,
1470
+ onClearReplyError,
1471
+ id
1472
+ }) {
1473
+ const optimistic = isOptimistic(ticket);
1474
+ const rawStatus = (ticket.status ?? "OPEN").toUpperCase();
1475
+ const priority = (ticket.priority ?? "").toUpperCase();
1476
+ const relativeUpdated = ticket.hubspot_updated_at ? formatRelativeTime(ticket.hubspot_updated_at) : "recently";
1477
+ const title = (ticket.subject || "").trim() || "(untitled)";
1478
+ const subtitle = `UPDATED ${relativeUpdated}, #${ticket.external_id || "\u2014"}${ticket.pipeline_stage_label ? `, ${ticket.pipeline_stage_label}` : ""}`;
1479
+ const description = ticket.preview ?? ticket.body ?? "";
1480
+ const isExpandable = !optimistic;
1481
+ const isExpanded = expanded && isExpandable;
1482
+ const rowRef = useRef3(null);
1483
+ const handleClick = useCallback5(() => {
1484
+ onToggle(ticket.id);
1485
+ }, [onToggle, ticket.id]);
1486
+ useEffect2(() => {
1487
+ if (!isExpanded) return;
1488
+ const raf = requestAnimationFrame(() => {
1489
+ scrollElementIntoView(rowRef.current, {
1490
+ headerOffset: STICKY_HEADER_OFFSET_PX
1491
+ });
1492
+ });
1493
+ return () => cancelAnimationFrame(raf);
1494
+ }, [isExpanded]);
1495
+ const rightBadges = /* @__PURE__ */ jsxs6(Fragment, { children: [
1496
+ /* @__PURE__ */ jsx7(
1497
+ StatusBadge,
1498
+ {
1499
+ text: rawStatus,
1500
+ colorScheme: getStatusColorScheme(rawStatus),
1501
+ variant: "card",
1502
+ className: "border border-ods-border"
1503
+ }
1504
+ ),
1505
+ priority && /* @__PURE__ */ jsx7(
1506
+ StatusBadge,
1507
+ {
1508
+ text: priority,
1509
+ colorScheme: mapPriorityScheme(priority),
1510
+ variant: "card",
1511
+ className: "border border-ods-border"
1512
+ }
1513
+ )
1514
+ ] });
1515
+ return /* @__PURE__ */ jsxs6(
1516
+ "div",
1517
+ {
1518
+ ref: rowRef,
1519
+ id,
1520
+ style: { scrollMarginTop: STICKY_HEADER_OFFSET_PX },
1521
+ className: `border-b border-ods-border last:border-b-0 ${optimistic ? "opacity-60" : ""}`,
1522
+ "aria-busy": optimistic || void 0,
1523
+ children: [
1524
+ /* @__PURE__ */ jsx7(
1525
+ "button",
1526
+ {
1527
+ type: "button",
1528
+ onClick: isExpandable ? handleClick : void 0,
1529
+ disabled: !isExpandable,
1530
+ "aria-expanded": isExpandable ? isExpanded : void 0,
1531
+ "aria-controls": isExpanded ? `help-center-drawer-${ticket.id}` : void 0,
1532
+ 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",
1533
+ children: /* @__PURE__ */ jsx7(
1534
+ DevCardRowContent,
1535
+ {
1536
+ title,
1537
+ subtitle,
1538
+ description,
1539
+ emptyDescription: "No description provided",
1540
+ rightBadges
1541
+ }
1542
+ )
1543
+ }
1544
+ ),
1545
+ isExpanded && /* @__PURE__ */ jsx7("div", { id: `help-center-drawer-${ticket.id}`, children: /* @__PURE__ */ jsx7(
1546
+ TicketDetailDrawer,
1547
+ {
1548
+ ticket,
1549
+ busy,
1550
+ supportSystemDown,
1551
+ onSendMessage,
1552
+ onClose,
1553
+ onReopen,
1554
+ onActionCollapsed,
1555
+ replyError,
1556
+ onClearReplyError
1557
+ }
1558
+ ) })
1559
+ ]
1560
+ }
1561
+ );
1562
+ }
1563
+ function mapPriorityScheme(priority) {
1564
+ if (priority === "HIGH" || priority === "URGENT") return "error";
1565
+ if (priority === "MEDIUM") return "warning";
1566
+ return "default";
1567
+ }
1568
+
1569
+ // src/components/tickets/help-center-create-form.tsx
1570
+ import { useState as useState5 } from "react";
1571
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1572
+ var SUBJECT_MAX_CHARS = 200;
1573
+ function HelpCenterCreateFormSkeleton() {
1574
+ 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: [
1575
+ /* @__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" }) }),
1576
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col flex-grow space-y-4 md:space-y-6", children: [
1577
+ /* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
1578
+ /* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
1579
+ /* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
1580
+ /* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
1581
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col", children: [
1582
+ /* @__PURE__ */ jsx8("div", { className: "h-[27px] w-20 bg-ods-border rounded animate-pulse mb-1" }),
1583
+ /* @__PURE__ */ jsx8("div", { className: "h-12 w-full bg-ods-border rounded animate-pulse" })
1584
+ ] }),
1585
+ /* @__PURE__ */ jsxs7("div", { className: "flex flex-col flex-grow", children: [
1586
+ /* @__PURE__ */ jsx8("div", { className: "h-[27px] w-32 bg-ods-border rounded animate-pulse mb-1" }),
1587
+ /* @__PURE__ */ jsx8("div", { className: "h-24 w-full bg-ods-border rounded animate-pulse flex-grow" })
1588
+ ] }),
1589
+ /* @__PURE__ */ jsx8("div", { className: "flex flex-col gap-2", children: /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
1590
+ /* @__PURE__ */ jsx8("div", { className: "h-7 w-7 bg-ods-border rounded animate-pulse shrink-0" }),
1591
+ /* @__PURE__ */ jsx8("div", { className: "h-4 w-40 bg-ods-border rounded animate-pulse" })
1592
+ ] }) }),
1593
+ /* @__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: [
1594
+ /* @__PURE__ */ jsx8("div", { className: "h-4 w-72 bg-ods-border rounded animate-pulse" }),
1595
+ /* @__PURE__ */ jsx8("div", { className: "h-12 w-32 bg-ods-border rounded animate-pulse" })
1596
+ ] })
1597
+ ] })
1598
+ ] });
1599
+ }
1600
+ function HelpCenterCreateForm({
1601
+ actions,
1602
+ sessionName,
1603
+ sessionEmail,
1604
+ supportSystemDown = false
1605
+ }) {
1606
+ const [subject, setSubject] = useState5("");
1607
+ const [subjectError, setSubjectError] = useState5(null);
1608
+ const subjectField = /* @__PURE__ */ jsxs7("div", { className: "flex flex-col", children: [
1609
+ /* @__PURE__ */ jsxs7(Label, { htmlFor: "help-center-subject", children: [
1610
+ "Subject",
1611
+ /* @__PURE__ */ jsx8("span", { className: "text-ods-accent", children: "*" })
1612
+ ] }),
1613
+ /* @__PURE__ */ jsx8(
1614
+ Input,
1615
+ {
1616
+ id: "help-center-subject",
1617
+ type: "text",
1618
+ value: subject,
1619
+ onChange: (e) => {
1620
+ setSubject(e.target.value);
1621
+ if (subjectError) setSubjectError(null);
1622
+ },
1623
+ placeholder: "Briefly describe what's going on",
1624
+ maxLength: SUBJECT_MAX_CHARS,
1625
+ "aria-invalid": !!subjectError,
1626
+ "aria-describedby": subjectError ? "help-center-subject-error" : void 0,
1627
+ disabled: supportSystemDown,
1628
+ className: "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12"
1629
+ }
1630
+ ),
1631
+ subjectError && /* @__PURE__ */ jsx8(
1632
+ "span",
1633
+ {
1634
+ id: "help-center-subject-error",
1635
+ className: "text-ods-error text-xs font-['DM_Sans'] mt-1",
1636
+ children: subjectError
1637
+ }
1638
+ )
1639
+ ] });
1640
+ return /* @__PURE__ */ jsx8(
1641
+ ContactForm,
1642
+ {
1643
+ title: "Open a new ticket",
1644
+ footerText: "The support team typically responds within one business day.",
1645
+ hideFields: ["name", "email", "companySize", "referralSource", "helpCategory"],
1646
+ defaultValues: {
1647
+ name: sessionName,
1648
+ email: sessionEmail,
1649
+ helpCategory: "Support Request"
1650
+ },
1651
+ extraTopField: subjectField,
1652
+ submitLabel: "Open ticket",
1653
+ attachmentsEnabled: true,
1654
+ onCustomSubmit: async (data, attachments) => {
1655
+ const trimmedSubject = subject.trim();
1656
+ if (!trimmedSubject) {
1657
+ setSubjectError("Subject is required");
1658
+ throw new Error("SUBJECT_REQUIRED");
1659
+ }
1660
+ setSubjectError(null);
1661
+ const ok = await actions.submitTicket({
1662
+ subject: trimmedSubject,
1663
+ content: data.message,
1664
+ attachments: attachments.length > 0 ? attachments : void 0
1665
+ });
1666
+ if (ok) {
1667
+ setSubject("");
1668
+ } else {
1669
+ throw new Error("TICKET_SUBMIT_FAILED");
1670
+ }
1671
+ }
1672
+ }
1673
+ );
1674
+ }
1675
+
1676
+ // src/components/tickets/help-center-list.tsx
1677
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1678
+ function HelpCenterList({ toast: toast2 = toast, backButton, title, shell } = {}) {
1679
+ const identity = useChatIdentity();
1680
+ const searchParams = useSearchParams();
1681
+ const router = useRouter();
1682
+ const pathname = usePathname();
1683
+ const search = searchParams.get("search") || "";
1684
+ const status = searchParams.get("status") || "all";
1685
+ const ticketParam = searchParams.get("ticket") || "";
1686
+ const rawPage = Number(searchParams.get("page"));
1687
+ const page = Number.isFinite(rawPage) && rawPage > 0 ? Math.floor(rawPage) : 1;
1688
+ if (identity.isLoading) {
1689
+ return /* @__PURE__ */ jsx9(
1690
+ DevSectionPage,
1691
+ {
1692
+ sectionKey: "tickets",
1693
+ backButton,
1694
+ title,
1695
+ shell,
1696
+ preControls: /* @__PURE__ */ jsx9(HelpCenterCreateFormSkeleton, {}),
1697
+ children: /* @__PURE__ */ jsx9(DevCardRowSkeletonList, {})
1698
+ }
1699
+ );
1700
+ }
1701
+ if (identity.authTier === "anon" || !identity.user?.email) {
1702
+ return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", backButton, title, shell, children: /* @__PURE__ */ jsx9(
1703
+ EmptyState,
1704
+ {
1705
+ type: "generic",
1706
+ title: "Sign in to manage tickets",
1707
+ description: "View, open, and follow up on support tickets after signing in.",
1708
+ showCTA: false
1709
+ }
1710
+ ) });
1711
+ }
1712
+ const sessionName = [identity.user?.firstName, identity.user?.lastName].filter(Boolean).join(" ").trim() || identity.user?.email?.split("@")[0] || "Customer";
1713
+ const sessionEmail = identity.user.email;
1714
+ return /* @__PURE__ */ jsx9(
1715
+ HelpCenterListAuthed,
1716
+ {
1717
+ search,
1718
+ status,
1719
+ page,
1720
+ ticketParam,
1721
+ searchParams,
1722
+ router,
1723
+ pathname,
1724
+ toast: toast2,
1725
+ sessionName,
1726
+ sessionEmail,
1727
+ backButton,
1728
+ title,
1729
+ shell
1730
+ }
1731
+ );
1732
+ }
1733
+ function HelpCenterListAuthed({
1734
+ search,
1735
+ status,
1736
+ page,
1737
+ ticketParam,
1738
+ searchParams,
1739
+ router,
1740
+ pathname,
1741
+ toast: toast2,
1742
+ sessionName,
1743
+ sessionEmail,
1744
+ backButton,
1745
+ title,
1746
+ shell
1747
+ }) {
1748
+ const queryClient = useQueryClient3();
1749
+ const [optimisticTickets, setOptimisticTickets] = useState6([]);
1750
+ const [supportSystemDown, setSupportSystemDown] = useState6(false);
1751
+ const setOpenTicket = useCallback6(
1752
+ (externalId) => {
1753
+ const params = new URLSearchParams(searchParams.toString());
1754
+ if (externalId) params.set("ticket", externalId);
1755
+ else params.delete("ticket");
1756
+ const qs = params.toString();
1757
+ router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
1758
+ },
1759
+ [searchParams, router, pathname]
1760
+ );
1761
+ const { tickets, isLoading, isFetching, error, refetch, totalPages } = useTicketsList({
1762
+ // `sessionEmail` is drilled in from the parent — see the same
1763
+ // pattern + race-cause rationale documented in
1764
+ // `HelpCenterCreateForm.sessionName/sessionEmail`. Calling
1765
+ // `useChatIdentity` inside `useTicketsList` would race the
1766
+ // parent's already-resolved identity and produce an empty-state
1767
+ // flash on first render.
1768
+ customerEmail: sessionEmail,
1769
+ search,
1770
+ status,
1771
+ page,
1772
+ // Live status: while a drawer is open, poll so an out-of-band HubSpot
1773
+ // status change (e.g. agent closes the ticket) flips the badge +
1774
+ // open/reopen affordance within one interval. Idle (no drawer) → no poll.
1775
+ // `ticketParam` (the open ticket's external_id) is the open signal.
1776
+ refetchInterval: ticketParam ? TICKET_LIVE_POLL_MS : false
1777
+ });
1778
+ const expandedTicketId = ticketParam && tickets.find((t) => t.external_id === ticketParam)?.id || null;
1779
+ const prependOptimistic = useCallback6((placeholder) => {
1780
+ setOptimisticTickets((prev) => [placeholder, ...prev]);
1781
+ }, []);
1782
+ const removeOptimistic = useCallback6((placeholderId) => {
1783
+ setOptimisticTickets((prev) => prev.filter((t) => t.id !== placeholderId));
1784
+ }, []);
1785
+ const removeTicketFromCache = useCallback6(
1786
+ (ticketId) => {
1787
+ queryClient.setQueriesData(
1788
+ { queryKey: ["tickets"] },
1789
+ (prev) => {
1790
+ if (!prev || !Array.isArray(prev.tickets)) return prev;
1791
+ const nextTickets = prev.tickets.filter((t) => t.id !== ticketId);
1792
+ if (nextTickets.length === prev.tickets.length) return prev;
1793
+ return { ...prev, tickets: nextTickets };
1794
+ }
1795
+ );
1796
+ },
1797
+ [queryClient]
1798
+ );
1799
+ const actions = useTicketActions({
1800
+ prependOptimistic,
1801
+ removeOptimistic,
1802
+ removeTicketFromCache,
1803
+ toast: toast2,
1804
+ onSupportSystemDown: () => setSupportSystemDown(true)
1805
+ });
1806
+ const toggleRow = useCallback6(
1807
+ (id) => {
1808
+ const t = tickets.find((x) => x.id === id);
1809
+ if (!t?.external_id) return;
1810
+ setOpenTicket(t.external_id === ticketParam ? null : t.external_id);
1811
+ },
1812
+ [tickets, ticketParam, setOpenTicket]
1813
+ );
1814
+ const merged = [...optimisticTickets, ...tickets];
1815
+ useScrollToHash(tickets, { headerOffset: STICKY_HEADER_OFFSET_PX });
1816
+ const hasActiveFilters = search !== "" || status !== "" && status !== "all";
1817
+ const hasResults = merged.length > 0;
1818
+ const form = /* @__PURE__ */ jsx9(
1819
+ HelpCenterCreateForm,
1820
+ {
1821
+ actions,
1822
+ sessionName,
1823
+ sessionEmail,
1824
+ supportSystemDown
1825
+ }
1826
+ );
1827
+ const body = /* @__PURE__ */ jsxs8("div", { className: "w-full flex flex-col gap-[40px]", children: [
1828
+ 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: [
1829
+ /* @__PURE__ */ jsxs8("p", { className: "text-ods-error text-base", children: [
1830
+ "Couldn\u2019t load your tickets. ",
1831
+ error.message
1832
+ ] }),
1833
+ /* @__PURE__ */ jsx9(Button, { type: "button", variant: "accent", onClick: () => refetch(), children: "Retry" })
1834
+ ] }),
1835
+ !error && /* @__PURE__ */ jsx9("div", { className: "w-full", children: isLoading ? /* @__PURE__ */ jsx9(DevCardRowSkeletonList, {}) : !hasResults && isFetching ? (
1836
+ // Bridge state — background refetch in flight and the
1837
+ // optimistic placeholder was just removed by the mutation
1838
+ // callback. Without this branch "No tickets yet" would flash
1839
+ // for ~50ms between `removeOptimistic` and the server
1840
+ // response landing.
1841
+ /* @__PURE__ */ jsx9(DevCardRowSkeletonList, { rows: 1 })
1842
+ ) : !hasResults ? hasActiveFilters ? /* @__PURE__ */ jsx9(
1843
+ EmptyState,
1844
+ {
1845
+ type: "search",
1846
+ title: "No tickets found",
1847
+ description: "No tickets match your current filters. Try clearing them or broadening your search.",
1848
+ showCTA: true,
1849
+ ctaText: "Reset filters",
1850
+ onCtaClick: () => {
1851
+ const params = new URLSearchParams(searchParams.toString());
1852
+ params.delete("search");
1853
+ params.delete("status");
1854
+ router.replace(`${pathname}?${params.toString()}`, { scroll: false });
1855
+ }
1856
+ }
1857
+ ) : /* @__PURE__ */ jsx9(
1858
+ EmptyState,
1859
+ {
1860
+ type: "generic",
1861
+ title: "No tickets yet",
1862
+ description: "Open one above to start the conversation with the support team.",
1863
+ showCTA: false
1864
+ }
1865
+ ) : (
1866
+ // `overflow-clip` (NOT `overflow-hidden`) — both visually
1867
+ // clip the rounded corners, but `hidden` makes the element
1868
+ // a "scroll container" per CSSOM spec, which causes
1869
+ // `scrollIntoView` calls inside (`<HelpCenterCard>` click
1870
+ // handlers) to try scrolling THIS div (can't, overflow
1871
+ // hidden) instead of bubbling up to the window. `clip`
1872
+ // keeps the visual clip but NOT the scroll-container
1873
+ // status, so click-to-scroll actually moves the page.
1874
+ /* @__PURE__ */ jsx9("div", { className: "bg-ods-card border border-ods-border rounded-[6px] overflow-clip w-full", children: merged.map((ticket) => /* @__PURE__ */ jsx9(
1875
+ HelpCenterCard,
1876
+ {
1877
+ id: devSectionAnchorId("ticket", ticket.external_id),
1878
+ ticket,
1879
+ expanded: expandedTicketId === ticket.id,
1880
+ onToggle: toggleRow,
1881
+ busy: isOptimistic(ticket) ? false : actions.isRowBusy(ticket.id),
1882
+ supportSystemDown,
1883
+ onSendMessage: actions.sendMessage,
1884
+ onClose: actions.closeTicket,
1885
+ onReopen: actions.reopenTicket,
1886
+ onActionCollapsed: () => setOpenTicket(null),
1887
+ replyError: actions.replyErrorFor(ticket.external_id),
1888
+ onClearReplyError: () => actions.clearReplyError(ticket.external_id)
1889
+ },
1890
+ ticket.id
1891
+ )) })
1892
+ ) }),
1893
+ !error && totalPages > 1 && /* @__PURE__ */ jsx9(UnifiedPagination, { currentPage: page, totalPages })
1894
+ ] });
1895
+ return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", backButton, title, shell, preControls: form, children: body });
1896
+ }
1897
+
1898
+ export {
1899
+ isOptimistic,
1900
+ TOAST_COPY,
1901
+ TicketOpenForm,
1902
+ useTicketEngagements,
1903
+ TicketLinkedDeliveryCard,
1904
+ TicketReplyComposer,
1905
+ TicketDetailDrawer,
1906
+ TicketRow,
1907
+ useTicketsList,
1908
+ useTicketActions,
1909
+ mapTicketActionError,
1910
+ TicketCenter,
1911
+ HelpCenterCard,
1912
+ HelpCenterCreateFormSkeleton,
1913
+ HelpCenterCreateForm,
1914
+ HelpCenterList
1915
+ };
1916
+ //# sourceMappingURL=chunk-M3NULYCR.js.map