fauna-api 0.0.0-stage → 1.0.676

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 (171) hide show
  1. package/dist/brands.d.ts +2 -0
  2. package/dist/brands.js +2 -0
  3. package/dist/index.d.ts +64 -0
  4. package/dist/index.js +121 -0
  5. package/dist/src/ant/chat.d.ts +130 -0
  6. package/dist/src/ant/chat.js +467 -0
  7. package/dist/src/ant/format.d.ts +40 -0
  8. package/dist/src/ant/format.js +143 -0
  9. package/dist/src/ant/graph.d.ts +130 -0
  10. package/dist/src/ant/graph.js +449 -0
  11. package/dist/src/ant/index.d.ts +16 -0
  12. package/dist/src/ant/index.js +61 -0
  13. package/dist/src/ant/persona.d.ts +151 -0
  14. package/dist/src/ant/persona.js +136 -0
  15. package/dist/src/ant/timeline.d.ts +283 -0
  16. package/dist/src/ant/timeline.js +1025 -0
  17. package/dist/src/authentification.d.ts +45 -0
  18. package/dist/src/authentification.js +194 -0
  19. package/dist/src/billing.d.ts +135 -0
  20. package/dist/src/billing.js +128 -0
  21. package/dist/src/calendar.d.ts +83 -0
  22. package/dist/src/calendar.js +133 -0
  23. package/dist/src/chats.d.ts +113 -0
  24. package/dist/src/chats.js +100 -0
  25. package/dist/src/cicada/contacts.d.ts +31 -0
  26. package/dist/src/cicada/contacts.js +28 -0
  27. package/dist/src/cicada/conversations.d.ts +73 -0
  28. package/dist/src/cicada/conversations.js +236 -0
  29. package/dist/src/cicada/inboxes.d.ts +48 -0
  30. package/dist/src/cicada/inboxes.js +82 -0
  31. package/dist/src/cicada/index.d.ts +16 -0
  32. package/dist/src/cicada/index.js +61 -0
  33. package/dist/src/cicada/phone.d.ts +48 -0
  34. package/dist/src/cicada/phone.js +306 -0
  35. package/dist/src/drive.d.ts +80 -0
  36. package/dist/src/drive.js +472 -0
  37. package/dist/src/extensions/index.d.ts +4 -0
  38. package/dist/src/extensions/index.js +5 -0
  39. package/dist/src/extensions/web/index.d.ts +6 -0
  40. package/dist/src/extensions/web/index.js +7 -0
  41. package/dist/src/extensions/web/session.d.ts +106 -0
  42. package/dist/src/extensions/web/session.js +469 -0
  43. package/dist/src/extensions/web/tools.d.ts +13 -0
  44. package/dist/src/extensions/web/tools.js +72 -0
  45. package/dist/src/friends.d.ts +39 -0
  46. package/dist/src/friends.js +119 -0
  47. package/dist/src/groups.d.ts +82 -0
  48. package/dist/src/groups.js +269 -0
  49. package/dist/src/inbox.d.ts +11 -0
  50. package/dist/src/inbox.js +35 -0
  51. package/dist/src/integrations.d.ts +52 -0
  52. package/dist/src/integrations.js +205 -0
  53. package/dist/src/magpie/explorer.d.ts +14 -0
  54. package/dist/src/magpie/explorer.js +36 -0
  55. package/dist/src/magpie/index.d.ts +16 -0
  56. package/dist/src/magpie/index.js +61 -0
  57. package/dist/src/magpie/portfolio.d.ts +52 -0
  58. package/dist/src/magpie/portfolio.js +179 -0
  59. package/dist/src/magpie/quotes.d.ts +35 -0
  60. package/dist/src/magpie/quotes.js +47 -0
  61. package/dist/src/magpie/room.d.ts +61 -0
  62. package/dist/src/magpie/room.js +238 -0
  63. package/dist/src/maps.d.ts +159 -0
  64. package/dist/src/maps.js +171 -0
  65. package/dist/src/marmot/browser.d.ts +139 -0
  66. package/dist/src/marmot/browser.js +389 -0
  67. package/dist/src/marmot/index.d.ts +20 -0
  68. package/dist/src/marmot/index.js +65 -0
  69. package/dist/src/marmot/media.d.ts +56 -0
  70. package/dist/src/marmot/media.js +265 -0
  71. package/dist/src/marmot/research.d.ts +337 -0
  72. package/dist/src/marmot/research.js +1138 -0
  73. package/dist/src/marmot/scraper.d.ts +59 -0
  74. package/dist/src/marmot/scraper.js +94 -0
  75. package/dist/src/marmot/web.d.ts +402 -0
  76. package/dist/src/marmot/web.js +2586 -0
  77. package/dist/src/marmot/workspace.d.ts +81 -0
  78. package/dist/src/marmot/workspace.js +263 -0
  79. package/dist/src/parrot/agent.d.ts +53 -0
  80. package/dist/src/parrot/agent.js +93 -0
  81. package/dist/src/parrot/chat.d.ts +84 -0
  82. package/dist/src/parrot/chat.js +237 -0
  83. package/dist/src/parrot/format.d.ts +128 -0
  84. package/dist/src/parrot/format.js +639 -0
  85. package/dist/src/parrot/generative.d.ts +261 -0
  86. package/dist/src/parrot/generative.js +185 -0
  87. package/dist/src/parrot/index.d.ts +22 -0
  88. package/dist/src/parrot/index.js +67 -0
  89. package/dist/src/parrot/room.d.ts +225 -0
  90. package/dist/src/parrot/room.js +466 -0
  91. package/dist/src/parrot/routing.d.ts +90 -0
  92. package/dist/src/parrot/routing.js +78 -0
  93. package/dist/src/parrot/share.d.ts +126 -0
  94. package/dist/src/parrot/share.js +261 -0
  95. package/dist/src/parrot/socket.d.ts +41 -0
  96. package/dist/src/parrot/socket.js +478 -0
  97. package/dist/src/payments.d.ts +73 -0
  98. package/dist/src/payments.js +107 -0
  99. package/dist/src/private/cards.css +75 -0
  100. package/dist/src/rooms.d.ts +76 -0
  101. package/dist/src/rooms.js +168 -0
  102. package/dist/src/shared/schema.d.ts +24 -0
  103. package/dist/src/shared/schema.js +1 -0
  104. package/dist/src/shared/server.d.ts +65 -0
  105. package/dist/src/shared/server.js +302 -0
  106. package/dist/src/termite/environment.d.ts +110 -0
  107. package/dist/src/termite/environment.js +263 -0
  108. package/dist/src/termite/index.d.ts +14 -0
  109. package/dist/src/termite/index.js +59 -0
  110. package/dist/src/termite/os.d.ts +326 -0
  111. package/dist/src/termite/os.js +572 -0
  112. package/dist/src/termite/versioning.d.ts +94 -0
  113. package/dist/src/termite/versioning.js +217 -0
  114. package/dist/src/tickets.d.ts +43 -0
  115. package/dist/src/tickets.js +138 -0
  116. package/dist/src/travel.d.ts +85 -0
  117. package/dist/src/travel.js +44 -0
  118. package/dist/src/ui/authentification.d.ts +24 -0
  119. package/dist/src/ui/authentification.js +177 -0
  120. package/dist/src/ui/brands.css +11 -0
  121. package/dist/src/ui/brands.d.ts +7 -0
  122. package/dist/src/ui/brands.js +14 -0
  123. package/dist/src/ui/browser.d.ts +74 -0
  124. package/dist/src/ui/browser.js +470 -0
  125. package/dist/src/ui/chat.d.ts +183 -0
  126. package/dist/src/ui/chat.js +2379 -0
  127. package/dist/src/ui/chatPanel.d.ts +109 -0
  128. package/dist/src/ui/chatPanel.js +768 -0
  129. package/dist/src/ui/chatSequence.d.ts +36 -0
  130. package/dist/src/ui/chatSequence.js +42 -0
  131. package/dist/src/ui/crown.d.ts +19 -0
  132. package/dist/src/ui/crown.js +199 -0
  133. package/dist/src/ui/cursor.d.ts +73 -0
  134. package/dist/src/ui/cursor.js +175 -0
  135. package/dist/src/ui/device.d.ts +33 -0
  136. package/dist/src/ui/device.js +49 -0
  137. package/dist/src/ui/drive.d.ts +69 -0
  138. package/dist/src/ui/drive.js +1243 -0
  139. package/dist/src/ui/generative.d.ts +39 -0
  140. package/dist/src/ui/generative.js +313 -0
  141. package/dist/src/ui/hover.d.ts +6 -0
  142. package/dist/src/ui/hover.js +210 -0
  143. package/dist/src/ui/index.d.ts +42 -0
  144. package/dist/src/ui/index.js +87 -0
  145. package/dist/src/ui/integrations.d.ts +13 -0
  146. package/dist/src/ui/integrations.js +175 -0
  147. package/dist/src/ui/maps.d.ts +87 -0
  148. package/dist/src/ui/maps.js +547 -0
  149. package/dist/src/ui/roster.d.ts +32 -0
  150. package/dist/src/ui/roster.js +163 -0
  151. package/dist/src/ui/talk.d.ts +12 -0
  152. package/dist/src/ui/talk.js +560 -0
  153. package/dist/src/ui/tickets.d.ts +13 -0
  154. package/dist/src/ui/tickets.js +966 -0
  155. package/dist/src/ui/toast.d.ts +7 -0
  156. package/dist/src/ui/toast.js +258 -0
  157. package/dist/src/ui/tokenText.d.ts +18 -0
  158. package/dist/src/ui/tokenText.js +96 -0
  159. package/dist/src/ui/tokenized.d.ts +42 -0
  160. package/dist/src/ui/tokenized.js +622 -0
  161. package/dist/src/ui/ui.css +4763 -0
  162. package/dist/src/ui/view.d.ts +23 -0
  163. package/dist/src/ui/view.js +584 -0
  164. package/dist/src/view.d.ts +113 -0
  165. package/dist/src/view.js +386 -0
  166. package/dist/src/webhooks.d.ts +66 -0
  167. package/dist/src/webhooks.js +78 -0
  168. package/dist/ui.d.ts +1 -0
  169. package/dist/ui.js +1 -0
  170. package/package.json +89 -6
  171. package/README.md +0 -3
@@ -0,0 +1,966 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { flushSync } from "react-dom";
5
+ import gsap from "gsap";
6
+ import { Fauna } from "fauna-api";
7
+ import { Crown } from "./brands.js";
8
+ import { Chat } from "./chat.js";
9
+ import { authorColorSeed, colorFromSeed, PinMark } from "./cursor.js";
10
+ import { parseRichText } from "./tokenized.js";
11
+ import { sendInformaticToast } from "./toast.js";
12
+ import { create, list, TicketRoom, ticketChatAutoEnabled } from "../tickets.js";
13
+ function ticketChatEmails(room) {
14
+ return room.getPresences().map((p) => p.profile.email ?? "");
15
+ }
16
+ function applyTicketChat(room, reporter, inst) {
17
+ const enabled = ticketChatAutoEnabled(ticketChatEmails(room), reporter ?? "");
18
+ inst?.setMode(enabled ? "assistant" : "thread");
19
+ inst?.setModels(enabled ? undefined : Fauna.Parrot.Generative.Text.AUTO);
20
+ }
21
+ const MIN_W = 320;
22
+ const MIN_H = 360;
23
+ const MARGIN = 8;
24
+ const EDGE_GOLD = "rgba(247, 239, 138, 0.28)";
25
+ const EDGE_CLEAR = "rgba(247, 239, 138, 0)";
26
+ const BAR_GOLD = "rgba(247, 239, 138, 0.12)";
27
+ const BAR_LINE = "rgba(247, 239, 138, 0.7)";
28
+ function lightChrome(el, on) {
29
+ gsap.killTweensOf(el);
30
+ if (el.classList.contains("tickets-panel-bar")) {
31
+ gsap.to(el, {
32
+ backgroundColor: on ? BAR_GOLD : EDGE_CLEAR,
33
+ borderBottomColor: on ? BAR_LINE : EDGE_CLEAR,
34
+ duration: 0.28,
35
+ ease: "power2.out",
36
+ });
37
+ return;
38
+ }
39
+ gsap.to(el, {
40
+ backgroundColor: on ? EDGE_GOLD : EDGE_CLEAR,
41
+ duration: 0.28,
42
+ ease: "power2.out",
43
+ });
44
+ }
45
+ const EDGES = [
46
+ { edge: "n", className: "tickets-resize-n" },
47
+ { edge: "s", className: "tickets-resize-s" },
48
+ { edge: "e", className: "tickets-resize-e" },
49
+ { edge: "w", className: "tickets-resize-w" },
50
+ { edge: "ne", className: "tickets-resize-ne" },
51
+ { edge: "nw", className: "tickets-resize-nw" },
52
+ { edge: "se", className: "tickets-resize-se" },
53
+ { edge: "sw", className: "tickets-resize-sw" },
54
+ ];
55
+ function defaultFrame() {
56
+ const w = Math.min(420, window.innerWidth - 32);
57
+ const h = Math.min(window.innerHeight * 0.7, window.innerHeight - 32);
58
+ return clampFrame({
59
+ x: window.innerWidth - w - 16,
60
+ y: (window.innerHeight - h) / 2,
61
+ w,
62
+ h,
63
+ });
64
+ }
65
+ function clampFrame(f) {
66
+ const w = Math.max(MIN_W, Math.min(f.w, window.innerWidth - MARGIN * 2));
67
+ const h = Math.max(MIN_H, Math.min(f.h, window.innerHeight - MARGIN * 2));
68
+ return {
69
+ w,
70
+ h,
71
+ x: Math.max(MARGIN, Math.min(f.x, window.innerWidth - w - MARGIN)),
72
+ y: Math.max(MARGIN, Math.min(f.y, window.innerHeight - h - MARGIN)),
73
+ };
74
+ }
75
+ function applyEdge(origin, edge, dx, dy) {
76
+ let { x, y, w, h } = origin;
77
+ if (edge.includes("e"))
78
+ w = origin.w + dx;
79
+ if (edge.includes("s"))
80
+ h = origin.h + dy;
81
+ if (edge.includes("w")) {
82
+ w = origin.w - dx;
83
+ x = origin.x + dx;
84
+ }
85
+ if (edge.includes("n")) {
86
+ h = origin.h - dy;
87
+ y = origin.y + dy;
88
+ }
89
+ if (w < MIN_W) {
90
+ if (edge.includes("w"))
91
+ x = origin.x + origin.w - MIN_W;
92
+ w = MIN_W;
93
+ }
94
+ if (h < MIN_H) {
95
+ if (edge.includes("n"))
96
+ y = origin.y + origin.h - MIN_H;
97
+ h = MIN_H;
98
+ }
99
+ return clampFrame({ x, y, w, h });
100
+ }
101
+ function relativeTime(ms) {
102
+ const s = Math.max(0, Math.round((Date.now() - ms) / 1000));
103
+ if (s < 45)
104
+ return "Just now";
105
+ const m = Math.round(s / 60);
106
+ if (m < 60)
107
+ return `${m}m ago`;
108
+ const h = Math.round(m / 60);
109
+ if (h < 24)
110
+ return `${h}h ago`;
111
+ const d = Math.round(h / 24);
112
+ if (d < 7)
113
+ return `${d}d ago`;
114
+ return new Date(ms).toLocaleDateString(undefined, { month: "short", day: "numeric" });
115
+ }
116
+ function lastPreview(ticket) {
117
+ const last = ticket.last;
118
+ if (!last)
119
+ return "No messages yet";
120
+ const texts = [];
121
+ for (const part of last.parts ?? []) {
122
+ if (!part || typeof part !== "object")
123
+ continue;
124
+ const row = part;
125
+ if (row.type === "TEXT" && typeof row.text === "string")
126
+ texts.push(row.text);
127
+ }
128
+ const raw = texts.join(" ").trim();
129
+ if (!raw)
130
+ return "No messages yet";
131
+ const plain = parseRichText(raw)
132
+ .filter((part) => part.kind === "text")
133
+ .map((part) => part.text)
134
+ .join(" ")
135
+ .replace(/\s+/g, " ")
136
+ .trim();
137
+ return plain || "No messages yet";
138
+ }
139
+ function sourceMatches(ticketSource, page) {
140
+ if (!ticketSource || !page)
141
+ return false;
142
+ try {
143
+ const a = new URL(ticketSource);
144
+ const b = new URL(page);
145
+ return a.origin === b.origin && a.pathname === b.pathname;
146
+ }
147
+ catch {
148
+ return ticketSource === page;
149
+ }
150
+ }
151
+ function pinFromToken(value, label) {
152
+ try {
153
+ const data = JSON.parse(value);
154
+ if (typeof data.x === "number" && typeof data.y === "number") {
155
+ return { x: data.x, y: data.y, note: data.note ?? label ?? "" };
156
+ }
157
+ }
158
+ catch { /* skip */ }
159
+ return null;
160
+ }
161
+ function pinsFromParts(parts) {
162
+ const found = [];
163
+ for (const part of parts) {
164
+ if (part.kind !== "token" || part.token.type !== "pin")
165
+ continue;
166
+ const pin = pinFromToken(part.token.value, part.token.label);
167
+ if (pin)
168
+ found.push(pin);
169
+ }
170
+ return found;
171
+ }
172
+ function pinsFromHistory(chat) {
173
+ if (!chat)
174
+ return [];
175
+ const found = [];
176
+ for (const message of chat.getHistory()) {
177
+ const author = authorColorSeed(message.author);
178
+ for (const pin of pinsFromParts(parseRichText(message.text ?? ""))) {
179
+ found.push({ ...pin, author });
180
+ }
181
+ }
182
+ return found;
183
+ }
184
+ function pinSnapshot(pins) {
185
+ return pins.map((pin) => JSON.stringify({ x: pin.x, y: pin.y, note: pin.note })).join("\0");
186
+ }
187
+ const CREATE_REST = {
188
+ backgroundColor: "#f7ef8a",
189
+ borderColor: "#f7ef8a",
190
+ color: "#111416",
191
+ y: 0,
192
+ boxShadow: "0 0 0 0 rgba(247, 239, 138, 0)",
193
+ };
194
+ const CREATE_HOVER = {
195
+ backgroundColor: "#fff8b8",
196
+ borderColor: "#fff8b8",
197
+ color: "#111416",
198
+ y: -1,
199
+ boxShadow: "0 8px 28px rgba(247, 239, 138, 0.11)",
200
+ };
201
+ function bindCreateHover(root) {
202
+ const enter = (e) => {
203
+ const btn = e.target.closest("button");
204
+ if (!btn || !root.contains(btn) || !btn.classList.contains("tickets-create"))
205
+ return;
206
+ gsap.killTweensOf(btn);
207
+ gsap.to(btn, { ...CREATE_HOVER, duration: 0.22, ease: "power2.out" });
208
+ };
209
+ const leave = (e) => {
210
+ const btn = e.target.closest("button");
211
+ if (!btn || !root.contains(btn) || !btn.classList.contains("tickets-create"))
212
+ return;
213
+ const next = e.relatedTarget;
214
+ if (next && btn.contains(next))
215
+ return;
216
+ gsap.killTweensOf(btn);
217
+ gsap.to(btn, { ...CREATE_REST, duration: 0.22, ease: "power2.out" });
218
+ };
219
+ root.addEventListener("mouseover", enter);
220
+ root.addEventListener("mouseout", leave);
221
+ return () => {
222
+ root.removeEventListener("mouseover", enter);
223
+ root.removeEventListener("mouseout", leave);
224
+ };
225
+ }
226
+ function PinDraft(props) {
227
+ const root = useRef(null);
228
+ const timeline = useRef(null);
229
+ const left = useRef(false);
230
+ const finish = () => {
231
+ if (left.current)
232
+ return;
233
+ left.current = true;
234
+ props.onLeft();
235
+ };
236
+ useLayoutEffect(() => {
237
+ const el = root.current;
238
+ if (!el)
239
+ return;
240
+ const clone = props.readOnly ? null : el.querySelector(".tickets-pin-clone");
241
+ const field = el.querySelector(".tickets-pin-field");
242
+ const actions = el.querySelector(".tickets-pin-actions");
243
+ const tl = gsap.timeline();
244
+ if (clone)
245
+ tl.fromTo(clone, { autoAlpha: 0, scale: 0.7 }, { autoAlpha: 1, scale: 1, duration: 0.22, ease: "power2.out" }, 0);
246
+ if (field) {
247
+ tl.fromTo(field, { autoAlpha: 0, scaleX: 0.28 }, { autoAlpha: 1, scaleX: 1, duration: 0.36, ease: "power3.out", transformOrigin: "0% 100%" }, 0);
248
+ }
249
+ if (actions) {
250
+ tl.fromTo(actions, { autoAlpha: 0, y: 8 }, { autoAlpha: 1, y: 0, duration: 0.28, ease: "power2.out" }, 0.12);
251
+ }
252
+ timeline.current = tl;
253
+ const undo = props.readOnly ? () => { } : bindCreateHover(el);
254
+ return () => {
255
+ undo();
256
+ tl.kill();
257
+ timeline.current = null;
258
+ };
259
+ }, []);
260
+ useLayoutEffect(() => {
261
+ const tl = timeline.current;
262
+ if (props.leaving) {
263
+ if (!tl) {
264
+ finish();
265
+ return;
266
+ }
267
+ tl.eventCallback("onReverseComplete", finish);
268
+ tl.reverse();
269
+ return;
270
+ }
271
+ if (!tl || !tl.reversed())
272
+ return;
273
+ tl.eventCallback("onReverseComplete", null);
274
+ left.current = false;
275
+ tl.play();
276
+ }, [props.leaving]);
277
+ return (_jsxs("div", { ref: root, className: `tickets-pin-draft${props.nested ? " is-nested" : ""}`, style: {
278
+ ...(props.nested ? undefined : { left: `${props.x * 100}%`, top: `${props.y * 100}%` }),
279
+ color: props.color,
280
+ }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), children: [!props.readOnly && (_jsx(PinMark, { className: "tickets-pin-clone" })), _jsx("div", { className: "tickets-pin-draft-inner", children: _jsxs("div", { className: "tickets-pin-stack", children: [_jsxs("div", { className: `tickets-pin-field${props.readOnly ? " is-readonly" : ""}`, children: [_jsx("span", { children: `${props.note || "Pin note"}\u200b` }), !props.readOnly && (_jsx("textarea", { autoFocus: true, rows: 1, value: props.note, placeholder: "Pin note", disabled: props.busy, onChange: (e) => props.onNote(e.target.value), onKeyDown: (e) => {
281
+ if (e.key === "Enter" && !e.shiftKey) {
282
+ e.preventDefault();
283
+ props.onCommit();
284
+ }
285
+ if (e.key === "Escape")
286
+ props.onCancel();
287
+ } }))] }), !props.readOnly && (_jsxs("div", { className: "tickets-pin-actions", children: [_jsx("button", { type: "button", className: "tickets-cancel", disabled: props.busy, onClick: props.onCancel, children: _jsx("span", { children: "Cancel" }) }), _jsxs("button", { type: "button", className: "tickets-create", disabled: props.busy, onClick: props.onCommit, children: [_jsx("span", { className: "material-symbols-outlined", children: "push_pin" }), _jsx("span", { children: "Pin" })] })] }))] }) })] }));
288
+ }
289
+ function pinToken(pin) {
290
+ return {
291
+ type: "pin",
292
+ value: JSON.stringify({ x: pin.x, y: pin.y, note: pin.note }),
293
+ label: pin.note.trim() || "Pin",
294
+ icon: "push_pin",
295
+ };
296
+ }
297
+ function Pins(props) {
298
+ const [pins, setPins] = useState([]);
299
+ const [draft, setDraft] = useState(null);
300
+ const [leaving, setLeaving] = useState(false);
301
+ const [peek, setPeek] = useState(null);
302
+ const [peekLeaving, setPeekLeaving] = useState(false);
303
+ const [note, setNote] = useState("");
304
+ const [cursor, setCursor] = useState(null);
305
+ const [busy, setBusy] = useState(false);
306
+ const nextDraft = useRef(null);
307
+ const draftSeq = useRef(0);
308
+ const peekRef = useRef(null);
309
+ const snapshot = useRef("");
310
+ const match = !props.ticket || sourceMatches(props.ticket.source, props.page);
311
+ useEffect(() => {
312
+ if (!match) {
313
+ if (snapshot.current === "")
314
+ return;
315
+ snapshot.current = "";
316
+ setPins([]);
317
+ return;
318
+ }
319
+ if (!props.chat)
320
+ return;
321
+ const apply = (parts) => {
322
+ const next = [
323
+ ...pinsFromHistory(props.chat),
324
+ ...pinsFromParts(parts ?? props.chat.getParts()).map((pin) => ({
325
+ ...pin,
326
+ author: pin.author || props.author,
327
+ })),
328
+ ];
329
+ const key = pinSnapshot(next);
330
+ if (key === snapshot.current)
331
+ return;
332
+ snapshot.current = key;
333
+ setPins(next);
334
+ };
335
+ apply();
336
+ return props.chat.watchInput((parts) => apply(parts));
337
+ }, [props.chat, props.revision, match, props.ticket?.id]);
338
+ if (!match)
339
+ return null;
340
+ const pointOf = (e) => ({
341
+ x: e.clientX / window.innerWidth,
342
+ y: e.clientY / window.innerHeight,
343
+ });
344
+ const overChrome = (e) => {
345
+ const target = e.target;
346
+ return Boolean(target.closest(".tickets-panel")
347
+ || target.closest(".tickets-pin-draft")
348
+ || target.closest(".tickets-pin-wrap"));
349
+ };
350
+ const closeDraft = (next) => {
351
+ nextDraft.current = next ?? null;
352
+ if (draft && !leaving)
353
+ setLeaving(true);
354
+ if (!draft) {
355
+ if (next) {
356
+ setDraft({ id: ++draftSeq.current, ...next });
357
+ setNote("");
358
+ }
359
+ }
360
+ };
361
+ const onDraftLeft = () => {
362
+ const next = nextDraft.current;
363
+ nextDraft.current = null;
364
+ setLeaving(false);
365
+ if (next) {
366
+ setDraft({ id: ++draftSeq.current, ...next });
367
+ setNote("");
368
+ return;
369
+ }
370
+ setDraft(null);
371
+ setNote("");
372
+ };
373
+ const onVignetteClick = (e) => {
374
+ if (overChrome(e) || busy)
375
+ return;
376
+ const point = pointOf(e);
377
+ setCursor(null);
378
+ if (draft) {
379
+ closeDraft(point);
380
+ return;
381
+ }
382
+ setDraft({ id: ++draftSeq.current, ...point });
383
+ setNote("");
384
+ };
385
+ const commit = async () => {
386
+ if (!draft || busy || leaving)
387
+ return;
388
+ const pin = { x: draft.x, y: draft.y, note: note.trim(), author: props.author };
389
+ if (props.chat) {
390
+ props.chat.insertToken(pinToken(pin));
391
+ closeDraft();
392
+ props.chat.focusInput();
393
+ return;
394
+ }
395
+ if (!props.onCreate)
396
+ return;
397
+ setBusy(true);
398
+ try {
399
+ await props.onCreate(pin);
400
+ setPins((prev) => [...prev, pin]);
401
+ closeDraft();
402
+ }
403
+ catch {
404
+ closeDraft();
405
+ }
406
+ finally {
407
+ setBusy(false);
408
+ }
409
+ };
410
+ return (_jsxs("div", { className: `tickets-pin-layer${cursor ? " is-aiming" : ""}`, onClick: onVignetteClick, onMouseMove: (e) => {
411
+ if (overChrome(e)) {
412
+ setCursor(null);
413
+ return;
414
+ }
415
+ setCursor(pointOf(e));
416
+ }, onMouseLeave: () => setCursor(null), children: [cursor && (_jsx("div", { className: "tickets-pin tickets-pin-cursor", style: { left: `${cursor.x * 100}%`, top: `${cursor.y * 100}%` }, children: _jsx(PinMark, {}) })), pins.map((pin, i) => (_jsxs("div", { className: `tickets-pin-wrap${peek === i ? " is-peeking" : ""}`, ref: (el) => {
417
+ const key = `${pin.x}:${pin.y}:${pin.note}`;
418
+ const target = el?.querySelector(".tickets-pin-wiggle") ?? el;
419
+ if (target)
420
+ props.pinRefs.current.set(key, target);
421
+ else
422
+ props.pinRefs.current.delete(key);
423
+ }, style: {
424
+ left: `${pin.x * 100}%`,
425
+ top: `${pin.y * 100}%`,
426
+ color: pin.author ? colorFromSeed(pin.author) : undefined,
427
+ }, onMouseEnter: () => {
428
+ peekRef.current = i;
429
+ setPeek(i);
430
+ setPeekLeaving(false);
431
+ }, onMouseLeave: () => {
432
+ if (peekRef.current === i)
433
+ setPeekLeaving(true);
434
+ }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), children: [_jsx("span", { className: "tickets-pin-wiggle", children: _jsx(PinMark, {}) }), peek === i && (_jsx(PinDraft, { readOnly: true, nested: true, color: pin.author ? colorFromSeed(pin.author) : undefined, x: pin.x, y: pin.y, note: pin.note, busy: false, leaving: peekLeaving, onNote: () => { }, onCommit: () => { }, onCancel: () => { }, onLeft: () => {
435
+ if (peekRef.current !== i)
436
+ return;
437
+ peekRef.current = null;
438
+ setPeek(null);
439
+ setPeekLeaving(false);
440
+ } }))] }, `${pin.x}:${pin.y}:${i}`))), draft && (_jsx(PinDraft, { x: draft.x, y: draft.y, color: props.author ? colorFromSeed(props.author) : undefined, note: note, busy: busy, leaving: leaving, onNote: setNote, onCommit: () => void commit(), onCancel: () => closeDraft(), onLeft: onDraftLeft }, draft.id))] }));
441
+ }
442
+ function Compose(props) {
443
+ return (_jsxs("div", { className: "tickets-compose", children: [_jsx("input", { value: props.title, placeholder: "What do you need help with", onChange: (e) => props.onTitle(e.target.value), onKeyDown: (e) => { if (e.key === "Enter")
444
+ props.onCreate(); } }), _jsxs("button", { type: "button", className: "tickets-create", disabled: props.busy, onClick: props.onCreate, children: [_jsx("span", { className: "material-symbols-outlined", children: "add" }), _jsx("span", { children: "New ticket" })] })] }));
445
+ }
446
+ function EmptyHero(props) {
447
+ const root = useRef(null);
448
+ useLayoutEffect(() => {
449
+ const el = root.current;
450
+ if (!el)
451
+ return;
452
+ const copy = el.querySelectorAll(".tickets-empty-copy > *");
453
+ const ghost = el.querySelector(".tickets-empty-ghost");
454
+ gsap.fromTo(copy, { y: 12, opacity: 0 }, { y: 0, opacity: 1, duration: 0.55, stagger: 0.055, ease: "power3.out" });
455
+ if (ghost)
456
+ gsap.fromTo(ghost, { y: 16, opacity: 0 }, { y: 0, opacity: 1, duration: 0.6, ease: "power3.out" });
457
+ return () => {
458
+ gsap.killTweensOf([copy, ghost]);
459
+ gsap.set(copy, { clearProps: "transform,opacity" });
460
+ if (ghost)
461
+ gsap.set(ghost, { clearProps: "transform,opacity" });
462
+ };
463
+ }, []);
464
+ return (_jsxs("div", { className: "tickets-empty-hero", ref: root, children: [_jsxs("div", { className: "tickets-empty-body", children: [_jsxs("div", { className: "tickets-empty-copy", children: [_jsx("div", { className: "tickets-empty-ornament", children: _jsx("span", { className: "tickets-empty-rule" }) }), _jsx("div", { className: "tickets-empty-kicker", children: "Need a hand" }), _jsx("div", { className: "tickets-empty-title", children: "Create a ticket" }), _jsx("div", { className: "tickets-empty-hint", children: "Tell us what is going on and we will pick it up." }), _jsxs("div", { className: "tickets-empty-features", children: [_jsx("span", { children: "Pin the page" }), _jsx("span", { children: "Chat with us" }), _jsx("span", { children: "Get an answer" })] })] }), _jsx("div", { className: "tickets-empty-visual", children: _jsxs("div", { className: "tickets-empty-ghost", children: [_jsxs("div", { className: "tickets-empty-ghost-head", children: [_jsx("span", { className: "tickets-empty-ghost-dot" }), _jsx("span", { className: "tickets-empty-ghost-id" }), _jsx("span", { className: "tickets-empty-ghost-title" }), _jsx("span", { className: "tickets-empty-ghost-status" })] }), _jsx("div", { className: "tickets-empty-ghost-line" }), _jsx("div", { className: "tickets-empty-ghost-line short" }), _jsxs("div", { className: "tickets-empty-ghost-foot", children: [_jsx("i", {}), _jsx("i", {})] })] }) })] }), _jsx(Compose, { title: props.title, busy: props.busy, onTitle: props.onTitle, onCreate: props.onCreate })] }));
465
+ }
466
+ export function Tickets(props) {
467
+ const page = props.source ?? (typeof location !== "undefined" ? location.href : "");
468
+ const [tickets, setTickets] = useState([]);
469
+ const pageTickets = tickets.filter((ticket) => sourceMatches(ticket.source, page));
470
+ const [loaded, setLoaded] = useState(false);
471
+ const [busy, setBusy] = useState(false);
472
+ const [title, setTitle] = useState("");
473
+ const [ticketRoom, setTicketRoom] = useState(null);
474
+ const [chatReady, setChatReady] = useState(null);
475
+ const [chat, setChat] = useState(null);
476
+ const chatInstRef = useRef(null);
477
+ const [pinRevision, setPinRevision] = useState(0);
478
+ const pinRefs = useRef(new Map());
479
+ const chatOptions = useRef(null);
480
+ const chatRoomRef = useRef(null);
481
+ const chatSelfRef = useRef(null);
482
+ const ticketRoomRef = useRef(null);
483
+ const [frame, setFrame] = useState(() => defaultFrame());
484
+ const vignetteRef = useRef(null);
485
+ const panelRef = useRef(null);
486
+ const boardRef = useRef(null);
487
+ const chatPaneRef = useRef(null);
488
+ const leavingRef = useRef(false);
489
+ const pagingRef = useRef(false);
490
+ const pendingPin = useRef(null);
491
+ const attachChat = useCallback((inst) => {
492
+ chatInstRef.current = inst;
493
+ setChat((prev) => (prev === inst ? prev : inst));
494
+ if (inst && chatRoomRef.current) {
495
+ applyTicketChat(chatRoomRef.current, ticketRoomRef.current?.ticket.reporter, inst);
496
+ }
497
+ }, []);
498
+ const dragRef = useRef(null);
499
+ const startDrag = (kind, e) => {
500
+ e.preventDefault();
501
+ window.getSelection()?.removeAllRanges();
502
+ const handle = e.currentTarget;
503
+ handle.setPointerCapture?.(e.pointerId);
504
+ document.body.style.userSelect = "none";
505
+ document.body.style.setProperty("-webkit-user-select", "none");
506
+ handle.closest(".tickets-overlay")?.classList.add("is-dragging");
507
+ dragRef.current = {
508
+ pointerId: e.pointerId,
509
+ kind,
510
+ origin: frame,
511
+ startX: e.clientX,
512
+ startY: e.clientY,
513
+ handle,
514
+ };
515
+ const onMove = (ev) => {
516
+ const drag = dragRef.current;
517
+ if (!drag || drag.pointerId !== ev.pointerId)
518
+ return;
519
+ ev.preventDefault();
520
+ const dx = ev.clientX - drag.startX;
521
+ const dy = ev.clientY - drag.startY;
522
+ if (drag.kind === "move") {
523
+ setFrame(clampFrame({ ...drag.origin, x: drag.origin.x + dx, y: drag.origin.y + dy }));
524
+ return;
525
+ }
526
+ setFrame(applyEdge(drag.origin, drag.kind, dx, dy));
527
+ };
528
+ const onUp = (ev) => {
529
+ if (dragRef.current?.pointerId !== ev.pointerId)
530
+ return;
531
+ const edge = dragRef.current.handle;
532
+ dragRef.current = null;
533
+ document.body.style.userSelect = "";
534
+ document.body.style.removeProperty("-webkit-user-select");
535
+ handle.closest(".tickets-overlay")?.classList.remove("is-dragging");
536
+ try {
537
+ handle.releasePointerCapture(ev.pointerId);
538
+ }
539
+ catch { /* noop */ }
540
+ if (edge && !edge.matches(":hover"))
541
+ lightChrome(edge, false);
542
+ window.removeEventListener("pointermove", onMove);
543
+ window.removeEventListener("pointerup", onUp);
544
+ window.removeEventListener("pointercancel", onUp);
545
+ };
546
+ window.addEventListener("pointermove", onMove);
547
+ window.addEventListener("pointerup", onUp);
548
+ window.addEventListener("pointercancel", onUp);
549
+ };
550
+ const startMove = (e) => {
551
+ if (e.target.closest("button"))
552
+ return;
553
+ startDrag("move", e);
554
+ };
555
+ const startResize = (edge, e) => {
556
+ e.stopPropagation();
557
+ startDrag(edge, e);
558
+ };
559
+ useEffect(() => {
560
+ const onResize = () => setFrame((prev) => clampFrame(prev));
561
+ window.addEventListener("resize", onResize);
562
+ return () => window.removeEventListener("resize", onResize);
563
+ }, []);
564
+ useLayoutEffect(() => {
565
+ const vignette = vignetteRef.current;
566
+ const panel = panelRef.current;
567
+ if (!vignette || !panel)
568
+ return;
569
+ gsap.set(vignette, { opacity: 0 });
570
+ gsap.set(panel, { x: 80, autoAlpha: 0 });
571
+ gsap.to(vignette, { opacity: 1, duration: 0.48, ease: "power2.out" });
572
+ gsap.to(panel, {
573
+ x: 0,
574
+ autoAlpha: 1,
575
+ duration: 0.52,
576
+ delay: 0.05,
577
+ ease: "power3.out",
578
+ boxShadow: "0 0 0 4px rgba(247, 239, 138, 0.16), 0 22px 48px -16px rgba(247, 239, 138, 0.28)",
579
+ });
580
+ return () => {
581
+ gsap.killTweensOf([vignette, panel]);
582
+ };
583
+ }, []);
584
+ useEffect(() => {
585
+ const panel = panelRef.current;
586
+ if (!panel)
587
+ return;
588
+ const restBorder = "rgba(247, 239, 138, 0.35)";
589
+ const hoverBorder = "rgba(247, 239, 138, 0.9)";
590
+ const restBg = "rgba(0, 0, 0, 0)";
591
+ const hoverBg = "rgba(247, 239, 138, 0.12)";
592
+ const stillInside = (btn, node) => Boolean(node && btn.contains(node));
593
+ const enter = (e) => {
594
+ const btn = e.target.closest("button");
595
+ if (!btn || !panel.contains(btn))
596
+ return;
597
+ if (stillInside(btn, e.relatedTarget))
598
+ return;
599
+ if (btn.classList.contains("tickets-row")) {
600
+ const line = btn.querySelector(".tickets-row-line");
601
+ if (!line)
602
+ return;
603
+ gsap.killTweensOf(line);
604
+ gsap.to(line, { scaleX: 1, duration: 0.34, ease: "power2.inOut" });
605
+ return;
606
+ }
607
+ gsap.killTweensOf(btn);
608
+ if (btn.classList.contains("tickets-create")) {
609
+ gsap.to(btn, { ...CREATE_HOVER, duration: 0.22, ease: "power2.out" });
610
+ return;
611
+ }
612
+ gsap.to(btn, { borderColor: hoverBorder, backgroundColor: hoverBg, duration: 0.28, ease: "power2.out" });
613
+ };
614
+ const leave = (e) => {
615
+ const btn = e.target.closest("button");
616
+ if (!btn || !panel.contains(btn))
617
+ return;
618
+ if (stillInside(btn, e.relatedTarget))
619
+ return;
620
+ if (btn.classList.contains("tickets-row")) {
621
+ const line = btn.querySelector(".tickets-row-line");
622
+ if (!line)
623
+ return;
624
+ gsap.killTweensOf(line);
625
+ gsap.to(line, { scaleX: 0, duration: 0.28, ease: "power2.inOut" });
626
+ return;
627
+ }
628
+ gsap.killTweensOf(btn);
629
+ if (btn.classList.contains("tickets-create")) {
630
+ gsap.to(btn, { ...CREATE_REST, duration: 0.22, ease: "power2.out" });
631
+ return;
632
+ }
633
+ gsap.to(btn, { borderColor: restBorder, backgroundColor: restBg, duration: 0.28, ease: "power2.out" });
634
+ };
635
+ panel.addEventListener("mouseover", enter);
636
+ panel.addEventListener("mouseout", leave);
637
+ const handles = Array.from(panel.querySelectorAll(".tickets-resize, .tickets-panel-bar"));
638
+ const drop = [];
639
+ for (const edge of handles) {
640
+ gsap.set(edge, edge.classList.contains("tickets-panel-bar")
641
+ ? { backgroundColor: EDGE_CLEAR, borderBottomColor: EDGE_CLEAR }
642
+ : { backgroundColor: EDGE_CLEAR });
643
+ const onEnter = () => lightChrome(edge, true);
644
+ const onLeave = () => {
645
+ if (dragRef.current?.handle === edge)
646
+ return;
647
+ lightChrome(edge, false);
648
+ };
649
+ edge.addEventListener("pointerenter", onEnter);
650
+ edge.addEventListener("pointerleave", onLeave);
651
+ drop.push(() => {
652
+ edge.removeEventListener("pointerenter", onEnter);
653
+ edge.removeEventListener("pointerleave", onLeave);
654
+ gsap.killTweensOf(edge);
655
+ });
656
+ }
657
+ return () => {
658
+ panel.removeEventListener("mouseover", enter);
659
+ panel.removeEventListener("mouseout", leave);
660
+ for (const undo of drop)
661
+ undo();
662
+ };
663
+ }, []);
664
+ const load = async () => {
665
+ try {
666
+ setTickets(await list(props.credentials));
667
+ }
668
+ catch (err) {
669
+ void sendInformaticToast(err?.message ?? "Could not load tickets.", "error");
670
+ }
671
+ finally {
672
+ setLoaded(true);
673
+ }
674
+ };
675
+ useEffect(() => { void load(); }, [props.credentials.token]);
676
+ useEffect(() => {
677
+ if (props.ticketId)
678
+ void openTicket(props.ticketId);
679
+ }, [props.ticketId]);
680
+ useEffect(() => {
681
+ return () => {
682
+ try {
683
+ chatRoomRef.current?.disconnect();
684
+ }
685
+ catch { /* noop */ }
686
+ try {
687
+ ticketRoomRef.current?.disconnect();
688
+ }
689
+ catch { /* noop */ }
690
+ };
691
+ }, []);
692
+ const wiggleTokenPin = (token) => {
693
+ if (token.type !== "pin")
694
+ return;
695
+ const parsed = pinFromToken(token.value, token.label);
696
+ if (!parsed)
697
+ return;
698
+ const el = pinRefs.current.get(`${parsed.x}:${parsed.y}:${parsed.note}`);
699
+ if (!el)
700
+ return;
701
+ gsap.killTweensOf(el);
702
+ gsap.timeline()
703
+ .to(el, { scale: 1.25, rotation: 12, duration: 0.12, ease: "power2.inOut" })
704
+ .to(el, { rotation: -10, duration: 0.1, ease: "power2.inOut" })
705
+ .to(el, { rotation: 8, duration: 0.1, ease: "power2.inOut" })
706
+ .to(el, { scale: 1, rotation: 0, duration: 0.18, ease: "power2.inOut" });
707
+ };
708
+ const refreshTicket = (next) => {
709
+ setChatReady((prev) => prev ? { ...prev, ticket: next.ticket } : prev);
710
+ };
711
+ const openTicket = async (id) => {
712
+ setBusy(true);
713
+ try {
714
+ let next;
715
+ try {
716
+ ;
717
+ ({ room: next } = await TicketRoom.connect(props.credentials, { id }, {
718
+ onPresence: (r) => {
719
+ refreshTicket(r);
720
+ },
721
+ onEvent: (r) => {
722
+ refreshTicket(r);
723
+ return true;
724
+ },
725
+ }));
726
+ }
727
+ catch (err) {
728
+ void sendInformaticToast(err?.message ?? "Could not open the ticket room.", "error");
729
+ return;
730
+ }
731
+ ticketRoomRef.current = next;
732
+ setTicketRoom(next);
733
+ let chatRoom;
734
+ let self;
735
+ try {
736
+ ;
737
+ ({ room: chatRoom, self } = await next.getChat({
738
+ onPresence: () => {
739
+ refreshTicket(next);
740
+ const room = chatRoomRef.current;
741
+ if (room)
742
+ applyTicketChat(room, next.ticket.reporter, chatInstRef.current);
743
+ },
744
+ onMessage: () => setPinRevision((n) => n + 1),
745
+ }));
746
+ }
747
+ catch (err) {
748
+ void sendInformaticToast(err?.message ?? "Could not open the chat room.", "error");
749
+ return;
750
+ }
751
+ chatRoomRef.current = chatRoom;
752
+ chatSelfRef.current = self;
753
+ applyTicketChat(chatRoom, next.ticket.reporter, chatInstRef.current);
754
+ const ready = {
755
+ room: chatRoom,
756
+ self,
757
+ ticket: next.ticket,
758
+ };
759
+ const enabled = ticketChatAutoEnabled(ticketChatEmails(chatRoom), next.ticket.reporter ?? "");
760
+ chatOptions.current = {
761
+ ...(enabled ? {} : { models: Fauna.Parrot.Generative.Text.AUTO }),
762
+ placeholder: "Write a message",
763
+ mode: enabled ? "assistant" : "thread",
764
+ onTokenClick: wiggleTokenPin,
765
+ room: {
766
+ room: chatRoom,
767
+ self,
768
+ onRoomMessage: () => setPinRevision((n) => n + 1),
769
+ onOlderLoaded: () => setPinRevision((n) => n + 1),
770
+ },
771
+ };
772
+ setChatReady(ready);
773
+ }
774
+ finally {
775
+ setBusy(false);
776
+ }
777
+ };
778
+ const createTicket = async (seed) => {
779
+ setBusy(true);
780
+ try {
781
+ const created = await create(props.credentials, {
782
+ title: (seed?.title ?? title).trim() || undefined,
783
+ source: page,
784
+ });
785
+ setTitle("");
786
+ setTickets((prev) => prev.some((row) => row.id === created.id) ? prev : [created, ...prev]);
787
+ if (seed?.pin)
788
+ pendingPin.current = seed.pin;
789
+ await openTicket(created.id);
790
+ }
791
+ catch (err) {
792
+ pendingPin.current = null;
793
+ void sendInformaticToast(err?.message ?? "Could not create ticket.", "error");
794
+ setBusy(false);
795
+ return false;
796
+ }
797
+ return true;
798
+ };
799
+ const teardownRoom = () => {
800
+ try {
801
+ chatReady?.room.disconnect();
802
+ }
803
+ catch { /* noop */ }
804
+ try {
805
+ ticketRoom?.disconnect();
806
+ }
807
+ catch { /* noop */ }
808
+ chatOptions.current = null;
809
+ chatRoomRef.current = null;
810
+ chatSelfRef.current = null;
811
+ ticketRoomRef.current = null;
812
+ setChat(null);
813
+ setChatReady(null);
814
+ setTicketRoom(null);
815
+ pendingPin.current = null;
816
+ void load();
817
+ };
818
+ const closeRoom = () => {
819
+ if (pagingRef.current)
820
+ return;
821
+ const board = boardRef.current;
822
+ const chatPane = chatPaneRef.current;
823
+ if (!chatReady || !chatPane) {
824
+ teardownRoom();
825
+ return;
826
+ }
827
+ pagingRef.current = true;
828
+ gsap.killTweensOf([board, chatPane].filter(Boolean));
829
+ const tl = gsap.timeline({
830
+ onComplete: () => {
831
+ pagingRef.current = false;
832
+ teardownRoom();
833
+ if (board)
834
+ gsap.set(board, { autoAlpha: 1, y: 0 });
835
+ },
836
+ });
837
+ tl.to(chatPane, { autoAlpha: 0, y: 16, duration: 0.28, ease: "power2.in" });
838
+ if (board)
839
+ tl.to(board, { autoAlpha: 1, y: 0, duration: 0.4, ease: "power3.out" }, "-=0.1");
840
+ };
841
+ const dismiss = () => {
842
+ if (leavingRef.current)
843
+ return;
844
+ leavingRef.current = true;
845
+ const vignette = vignetteRef.current;
846
+ const panel = panelRef.current;
847
+ const finish = () => {
848
+ teardownRoom();
849
+ props.onClose?.();
850
+ };
851
+ if (!vignette || !panel) {
852
+ finish();
853
+ return;
854
+ }
855
+ gsap.killTweensOf([vignette, panel]);
856
+ gsap.to(vignette, { opacity: 0, duration: 0.32, ease: "power2.in" });
857
+ gsap.to(panel, {
858
+ x: 80,
859
+ autoAlpha: 0,
860
+ duration: 0.32,
861
+ ease: "power2.in",
862
+ boxShadow: "0 0 0 0 rgba(247, 239, 138, 0), 0 18px 48px -24px rgba(0, 0, 0, 0.55)",
863
+ onComplete: finish,
864
+ });
865
+ };
866
+ const chatOpen = Boolean(chatReady);
867
+ useLayoutEffect(() => {
868
+ const board = boardRef.current;
869
+ const chatPane = chatPaneRef.current;
870
+ if (!chatOpen || !chatPane)
871
+ return;
872
+ pagingRef.current = true;
873
+ gsap.killTweensOf([board, chatPane].filter(Boolean));
874
+ gsap.set(chatPane, { autoAlpha: 0, y: 20 });
875
+ const tl = gsap.timeline({
876
+ onComplete: () => { pagingRef.current = false; },
877
+ });
878
+ if (board)
879
+ tl.to(board, { autoAlpha: 0, y: -14, duration: 0.28, ease: "power2.in" }, 0);
880
+ tl.to(chatPane, { autoAlpha: 1, y: 0, duration: 0.44, ease: "power3.out" }, 0.08);
881
+ return () => {
882
+ gsap.killTweensOf([board, chatPane].filter(Boolean));
883
+ pagingRef.current = false;
884
+ };
885
+ }, [chatOpen]);
886
+ useEffect(() => {
887
+ if (!chat)
888
+ return;
889
+ setPinRevision((n) => n + 1);
890
+ const pin = pendingPin.current;
891
+ if (!pin)
892
+ return;
893
+ pendingPin.current = null;
894
+ chat.insertToken(pinToken(pin));
895
+ chat.focusInput();
896
+ }, [chat]);
897
+ return (_jsxs("div", { className: "tickets-overlay", children: [_jsx("div", { className: "tickets-vignette", ref: vignetteRef }), _jsx(Pins, { chat: chat, ticket: chatReady?.ticket, page: page, revision: pinRevision, author: props.credentials.email, pinRefs: pinRefs, onCreate: async (pin) => {
898
+ if (!await createTicket({ title: pin.note || undefined, pin })) {
899
+ throw new Error("Could not create ticket.");
900
+ }
901
+ } }), _jsxs("div", { ref: panelRef, className: "tickets-panel", style: { left: frame.x, top: frame.y, width: frame.w, height: frame.h }, children: [EDGES.map((handle) => (_jsx("div", { className: `tickets-resize ${handle.className}`, onPointerDown: (e) => startResize(handle.edge, e) }, handle.edge))), _jsxs("div", { className: "tickets-panel-bar", onPointerDown: startMove, children: [_jsxs("span", { className: "tickets-panel-brand", children: [_jsx(Crown, {}), _jsx("span", { children: chatReady ? (chatReady.ticket.title || "Ticket") : "Support" })] }), _jsxs("div", { className: "tickets-panel-actions", children: [chatReady && !props.ticketId && (_jsxs("button", { type: "button", className: "tickets-create", onClick: closeRoom, children: [_jsx("span", { className: "material-symbols-outlined", children: "list" }), _jsx("span", { children: "Tickets" })] })), (!chatReady || props.ticketId) && (_jsxs("button", { type: "button", className: "tickets-create", onClick: dismiss, children: [_jsx("span", { className: "material-symbols-outlined", children: "close" }), _jsx("span", { children: "Close" })] }))] })] }), _jsxs("div", { className: "tickets-stage", children: [_jsxs("div", { ref: boardRef, className: "tickets-stage-pane", "aria-hidden": chatOpen, children: [loaded && pageTickets.length === 0 && (_jsx(EmptyHero, { title: title, busy: busy, onTitle: setTitle, onCreate: () => void createTicket() })), pageTickets.length > 0 && (_jsxs("div", { className: "tickets-board", children: [_jsx("div", { className: "tickets-board-head", children: "Open tickets" }), _jsx("div", { className: "tickets-board-rule" }), _jsx("div", { className: "tickets-list", children: pageTickets.map((ticket) => (_jsxs("button", { type: "button", className: `tickets-row${ticket.unread ? " is-unread" : ""}`, disabled: busy, onClick: () => void openTicket(ticket.id), children: [_jsxs("span", { className: "tickets-row-top", children: [_jsx("strong", { children: ticket.title || "Untitled" }), _jsx("time", { children: relativeTime(ticket.last?.createdAt.getTime() ?? ticket.updatedAt.getTime()) })] }), _jsx("span", { className: "tickets-row-preview", children: lastPreview(ticket) }), _jsx("i", { className: "tickets-row-line", "aria-hidden": true })] }, ticket.id))) }), _jsx("div", { className: "tickets-board-rule" }), _jsx(Compose, { title: title, busy: busy, onTitle: setTitle, onCreate: () => void createTicket() })] }))] }), chatReady && chatOptions.current && (_jsx("div", { ref: chatPaneRef, className: "tickets-stage-pane tickets-chat", children: _jsx(Chat, { ref: attachChat, options: chatOptions.current }) }))] })] })] }));
902
+ }
903
+ export function Support(props) {
904
+ const [open, setOpen] = useState(false);
905
+ const [ready, setReady] = useState(false);
906
+ const [unread, setUnread] = useState(false);
907
+ const btn = useRef(null);
908
+ const busy = useRef(false);
909
+ const seen = useRef(false);
910
+ useEffect(() => {
911
+ if (open)
912
+ return;
913
+ let live = true;
914
+ void list(props.credentials)
915
+ .then((rows) => {
916
+ if (live)
917
+ setUnread(rows.some((ticket) => ticket.unread));
918
+ })
919
+ .catch(() => { })
920
+ .finally(() => { if (live)
921
+ setReady(true); });
922
+ return () => { live = false; };
923
+ }, [props.credentials.token, open]);
924
+ const openSupport = () => {
925
+ if (busy.current || open)
926
+ return;
927
+ const el = btn.current;
928
+ if (!el) {
929
+ setOpen(true);
930
+ return;
931
+ }
932
+ busy.current = true;
933
+ gsap.killTweensOf(el);
934
+ gsap.timeline({
935
+ onComplete: () => {
936
+ setOpen(true);
937
+ busy.current = false;
938
+ },
939
+ })
940
+ .to(el, { y: -18, duration: 0.16, ease: "power2.out" })
941
+ .to(el, { y: 96, autoAlpha: 0, duration: 0.28, ease: "power2.in" });
942
+ };
943
+ useLayoutEffect(() => {
944
+ const el = btn.current;
945
+ if (!el || open)
946
+ return;
947
+ gsap.killTweensOf(el);
948
+ if (!seen.current) {
949
+ seen.current = true;
950
+ gsap.set(el, { y: 0, autoAlpha: 1 });
951
+ return;
952
+ }
953
+ gsap.fromTo(el, { y: 96, autoAlpha: 0 }, { y: 0, autoAlpha: 1, duration: 0.42, ease: "back.out(1.7)" });
954
+ }, [open]);
955
+ return (_jsxs(_Fragment, { children: [ready && (_jsxs("button", { ref: btn, type: "button", className: `tickets-support${unread ? " is-unread" : ""}`, onClick: openSupport, style: { pointerEvents: open ? "none" : "auto" }, tabIndex: open ? -1 : 0, "aria-hidden": open, children: [_jsx(Crown, {}), _jsx("span", { children: "Support" }), unread && _jsx("i", { className: "tickets-support-dot" })] })), open && (_jsx(Tickets, { ...props, onClose: () => {
956
+ setOpen(false);
957
+ props.onClose?.();
958
+ } }))] }));
959
+ }
960
+ export function createTickets(container, options) {
961
+ const root = createRoot(container);
962
+ flushSync(() => root.render(_jsx(Tickets, { ...options })));
963
+ return {
964
+ destroy: () => root.unmount(),
965
+ };
966
+ }