@flamingo-stack/openframe-frontend-core 0.0.314 → 0.0.315-snapshot.20260624135932

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