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,622 @@
1
+ import gsap from "gsap";
2
+ import { Fauna } from "fauna-api";
3
+ import { serializeParts } from "./tokenText.js";
4
+ export { encodeToken, serializeParts, parseRichText, hasTokens, toPlainText } from "./tokenText.js";
5
+ // ─── createTokenizedInput ────────────────────────────────────────────────────
6
+ export function createTokenizedInput(container, options = {}) {
7
+ const triggers = options.triggers ?? [];
8
+ const triggerChars = new Set(triggers.map(t => t.char));
9
+ // ── DOM ──
10
+ const root = document.createElement('div');
11
+ root.className = 'tokenized-root';
12
+ const editor = document.createElement('div');
13
+ editor.className = 'tokenized-editor';
14
+ editor.contentEditable = 'true';
15
+ editor.setAttribute('role', 'textbox');
16
+ editor.setAttribute('aria-multiline', options.multiline ? 'true' : 'false');
17
+ if (options.placeholder) {
18
+ editor.dataset.placeholder = options.placeholder;
19
+ }
20
+ root.appendChild(editor);
21
+ const suggestionsEl = document.createElement('div');
22
+ suggestionsEl.className = 'tokenized-suggestions';
23
+ suggestionsEl.style.display = 'none';
24
+ root.appendChild(suggestionsEl);
25
+ container.appendChild(root);
26
+ // ── Suggestion state ──
27
+ let activeTrigger = null;
28
+ let triggerAnchorNode = null;
29
+ let triggerAnchorOffset = 0;
30
+ let suggestionItems = [];
31
+ let activeIndex = 0;
32
+ let suggestGeneration = 0;
33
+ // ── Helpers ──
34
+ function parseParts() {
35
+ const parts = [];
36
+ function walk(node) {
37
+ if (node.nodeType === Node.TEXT_NODE) {
38
+ const text = node.textContent ?? '';
39
+ if (text.length > 0) {
40
+ // Merge consecutive text parts
41
+ const last = parts[parts.length - 1];
42
+ if (last && last.kind === 'text') {
43
+ last.text += text;
44
+ }
45
+ else {
46
+ parts.push({ kind: 'text', text });
47
+ }
48
+ }
49
+ }
50
+ else if (node.nodeType === Node.ELEMENT_NODE) {
51
+ const el = node;
52
+ if (el.classList.contains('tokenized-token')) {
53
+ parts.push({
54
+ kind: 'token',
55
+ token: {
56
+ type: el.dataset.type ?? '',
57
+ value: el.dataset.value ?? '',
58
+ label: el.dataset.label ?? '',
59
+ icon: el.dataset.icon || undefined
60
+ }
61
+ });
62
+ }
63
+ else if (el.tagName === 'BR') {
64
+ const last = parts[parts.length - 1];
65
+ if (last && last.kind === 'text') {
66
+ last.text += '\n';
67
+ }
68
+ else {
69
+ parts.push({ kind: 'text', text: '\n' });
70
+ }
71
+ }
72
+ else {
73
+ // Recurse into non-token elements (e.g. divs from contenteditable line wrapping)
74
+ for (const child of node.childNodes) {
75
+ walk(child);
76
+ }
77
+ // Contenteditable wraps lines in divs — add newline after block elements
78
+ if (el.tagName === 'DIV' || el.tagName === 'P') {
79
+ const last = parts[parts.length - 1];
80
+ if (last && last.kind === 'text' && !last.text.endsWith('\n')) {
81
+ last.text += '\n';
82
+ }
83
+ else if (!last || last.kind !== 'text') {
84
+ parts.push({ kind: 'text', text: '\n' });
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ for (const child of editor.childNodes) {
91
+ walk(child);
92
+ }
93
+ // Trim trailing newline
94
+ const last = parts[parts.length - 1];
95
+ if (last && last.kind === 'text') {
96
+ last.text = last.text.replace(/\n+$/, '');
97
+ if (last.text.length === 0)
98
+ parts.pop();
99
+ }
100
+ return parts;
101
+ }
102
+ function serializeText(parts) {
103
+ return serializeParts(parts);
104
+ }
105
+ function buildTokenSpan(token) {
106
+ const span = document.createElement('span');
107
+ span.className = 'tokenized-token';
108
+ span.contentEditable = 'false';
109
+ span.dataset.type = token.type;
110
+ span.dataset.value = token.value;
111
+ span.dataset.label = token.label;
112
+ if (token.icon)
113
+ span.dataset.icon = token.icon;
114
+ if (token.icon) {
115
+ const iconEl = document.createElement('span');
116
+ iconEl.className = 'material-symbols-outlined tokenized-token-icon';
117
+ iconEl.textContent = token.icon;
118
+ span.appendChild(iconEl);
119
+ }
120
+ const labelEl = document.createElement('span');
121
+ labelEl.className = 'tokenized-token-label';
122
+ labelEl.textContent = token.label;
123
+ span.appendChild(labelEl);
124
+ const removeEl = document.createElement('span');
125
+ removeEl.className = 'material-symbols-outlined tokenized-token-remove';
126
+ removeEl.textContent = 'close';
127
+ removeEl.onmousedown = (e) => {
128
+ e.preventDefault();
129
+ e.stopPropagation();
130
+ gsap.to(span, {
131
+ duration: 0.2, opacity: 0, scale: 0.8, ease: 'power2.in',
132
+ onComplete: () => {
133
+ // Remove the trailing zero-width space if present
134
+ const next = span.nextSibling;
135
+ if (next && next.nodeType === Node.TEXT_NODE && next.textContent === '\u200B') {
136
+ next.remove();
137
+ }
138
+ span.remove();
139
+ fireInput();
140
+ }
141
+ });
142
+ };
143
+ span.appendChild(removeEl);
144
+ return span;
145
+ }
146
+ function insertTokenAtCaret(token) {
147
+ const span = buildTokenSpan(token);
148
+ const zwsp = document.createTextNode('\u200B');
149
+ const sel = window.getSelection();
150
+ const range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
151
+ // Only honor the caret when it actually lives inside this editor.
152
+ // Otherwise the token would be injected wherever the document selection
153
+ // happens to be (e.g. up in the page header) — so append into the editor
154
+ // instead and drop the caret after it.
155
+ if (!range || !editor.contains(range.commonAncestorContainer)) {
156
+ editor.appendChild(span);
157
+ editor.appendChild(zwsp);
158
+ if (sel) {
159
+ const end = document.createRange();
160
+ end.setStartAfter(zwsp);
161
+ end.collapse(true);
162
+ sel.removeAllRanges();
163
+ sel.addRange(end);
164
+ }
165
+ }
166
+ else {
167
+ range.deleteContents();
168
+ range.insertNode(zwsp);
169
+ range.insertNode(span);
170
+ // Move caret after zero-width space
171
+ range.setStartAfter(zwsp);
172
+ range.setEndAfter(zwsp);
173
+ sel.removeAllRanges();
174
+ sel.addRange(range);
175
+ }
176
+ gsap.from(span, { duration: 0.25, opacity: 0, scale: 0.85, ease: 'power2.out' });
177
+ fireInput();
178
+ }
179
+ function fireInput() {
180
+ options.onInput?.(parseParts());
181
+ }
182
+ // ── Suggestion popup ──
183
+ function showSuggestions(items) {
184
+ suggestionItems = items;
185
+ activeIndex = 0;
186
+ suggestionsEl.innerHTML = '';
187
+ if (items.length === 0) {
188
+ hideSuggestions();
189
+ return;
190
+ }
191
+ for (let i = 0; i < items.length; i++) {
192
+ const row = document.createElement('div');
193
+ row.className = 'tokenized-suggestion';
194
+ if (i === 0)
195
+ row.classList.add('active');
196
+ if (items[i].image) {
197
+ const img = document.createElement('img');
198
+ img.className = 'tokenized-suggestion-image';
199
+ img.src = items[i].image;
200
+ row.appendChild(img);
201
+ }
202
+ else if (items[i].icon) {
203
+ const icon = document.createElement('span');
204
+ icon.className = 'material-symbols-outlined tokenized-suggestion-icon';
205
+ icon.textContent = items[i].icon;
206
+ row.appendChild(icon);
207
+ }
208
+ const text = document.createElement('div');
209
+ text.className = 'tokenized-suggestion-text';
210
+ const label = document.createElement('div');
211
+ label.className = 'tokenized-suggestion-label';
212
+ label.textContent = items[i].label;
213
+ text.appendChild(label);
214
+ if (items[i].subtitle) {
215
+ const sub = document.createElement('div');
216
+ sub.className = 'tokenized-suggestion-subtitle';
217
+ sub.textContent = items[i].subtitle;
218
+ text.appendChild(sub);
219
+ }
220
+ row.appendChild(text);
221
+ row.onmousedown = (e) => {
222
+ e.preventDefault();
223
+ pickSuggestion(i);
224
+ };
225
+ suggestionsEl.appendChild(row);
226
+ }
227
+ // Position popup near caret
228
+ positionSuggestions();
229
+ suggestionsEl.style.display = 'flex';
230
+ gsap.fromTo(suggestionsEl, { opacity: 0, y: 4 }, { opacity: 1, y: 0, duration: 0.15, ease: 'power2.out' });
231
+ }
232
+ function positionSuggestions() {
233
+ const sel = window.getSelection();
234
+ if (!sel || sel.rangeCount === 0)
235
+ return;
236
+ const range = sel.getRangeAt(0).cloneRange();
237
+ range.collapse(true);
238
+ const rect = range.getBoundingClientRect();
239
+ const rootRect = root.getBoundingClientRect();
240
+ if (rect.width === 0 && rect.height === 0) {
241
+ // Fallback — position at bottom-left of editor
242
+ suggestionsEl.style.left = '0px';
243
+ suggestionsEl.style.bottom = `${root.offsetHeight - editor.offsetTop}px`;
244
+ return;
245
+ }
246
+ suggestionsEl.style.left = `${rect.left - rootRect.left}px`;
247
+ suggestionsEl.style.bottom = `${rootRect.bottom - rect.top + 4}px`;
248
+ }
249
+ function hideSuggestions() {
250
+ activeTrigger = null;
251
+ triggerAnchorNode = null;
252
+ suggestionItems = [];
253
+ suggestGeneration++;
254
+ suggestionsEl.style.display = 'none';
255
+ suggestionsEl.innerHTML = '';
256
+ }
257
+ function updateActiveRow() {
258
+ const rows = suggestionsEl.querySelectorAll('.tokenized-suggestion');
259
+ rows.forEach((row, i) => {
260
+ row.classList.toggle('active', i === activeIndex);
261
+ });
262
+ // Scroll active into view
263
+ const activeRow = rows[activeIndex];
264
+ activeRow?.scrollIntoView({ block: 'nearest' });
265
+ }
266
+ function pickSuggestion(index) {
267
+ if (!activeTrigger || index < 0 || index >= suggestionItems.length) {
268
+ hideSuggestions();
269
+ return;
270
+ }
271
+ const token = activeTrigger.onPick(suggestionItems[index]);
272
+ // Delete the trigger char + query text before inserting the token
273
+ const sel = window.getSelection();
274
+ if (sel && sel.rangeCount > 0 && triggerAnchorNode) {
275
+ const range = document.createRange();
276
+ range.setStart(triggerAnchorNode, triggerAnchorOffset);
277
+ range.setEnd(sel.focusNode, sel.focusOffset);
278
+ range.deleteContents();
279
+ sel.removeAllRanges();
280
+ sel.addRange(range);
281
+ }
282
+ hideSuggestions();
283
+ insertTokenAtCaret(token);
284
+ }
285
+ function getTriggerQuery() {
286
+ if (!activeTrigger || !triggerAnchorNode)
287
+ return null;
288
+ const sel = window.getSelection();
289
+ if (!sel || sel.rangeCount === 0)
290
+ return null;
291
+ // Build text from anchor to current caret
292
+ try {
293
+ const range = document.createRange();
294
+ range.setStart(triggerAnchorNode, triggerAnchorOffset);
295
+ range.setEnd(sel.focusNode, sel.focusOffset);
296
+ const text = range.toString();
297
+ // Should start with the trigger char
298
+ if (!text.startsWith(activeTrigger.char)) {
299
+ return null;
300
+ }
301
+ return text.slice(activeTrigger.char.length);
302
+ }
303
+ catch {
304
+ return null;
305
+ }
306
+ }
307
+ async function runSuggest(query) {
308
+ if (!activeTrigger)
309
+ return;
310
+ suggestGeneration++;
311
+ const gen = suggestGeneration;
312
+ const trigger = activeTrigger;
313
+ try {
314
+ const results = await trigger.suggest(query);
315
+ if (gen !== suggestGeneration || activeTrigger !== trigger)
316
+ return;
317
+ showSuggestions(results);
318
+ }
319
+ catch {
320
+ if (gen === suggestGeneration)
321
+ hideSuggestions();
322
+ }
323
+ }
324
+ // Debounce for suggest calls
325
+ let suggestTimer = null;
326
+ function debounceSuggest(query) {
327
+ if (suggestTimer)
328
+ clearTimeout(suggestTimer);
329
+ suggestTimer = setTimeout(() => runSuggest(query), 120);
330
+ }
331
+ // ── Input events ──
332
+ editor.addEventListener('input', () => {
333
+ fireInput();
334
+ if (activeTrigger) {
335
+ const query = getTriggerQuery();
336
+ if (query === null) {
337
+ hideSuggestions();
338
+ }
339
+ else {
340
+ debounceSuggest(query);
341
+ }
342
+ }
343
+ });
344
+ editor.addEventListener('keydown', (e) => {
345
+ // Suggestion navigation
346
+ if (suggestionsEl.style.display !== 'none' && suggestionItems.length > 0) {
347
+ if (e.key === 'ArrowDown') {
348
+ e.preventDefault();
349
+ activeIndex = (activeIndex + 1) % suggestionItems.length;
350
+ updateActiveRow();
351
+ return;
352
+ }
353
+ if (e.key === 'ArrowUp') {
354
+ e.preventDefault();
355
+ activeIndex = (activeIndex - 1 + suggestionItems.length) % suggestionItems.length;
356
+ updateActiveRow();
357
+ return;
358
+ }
359
+ if (e.key === 'Enter' || e.key === 'Tab') {
360
+ e.preventDefault();
361
+ pickSuggestion(activeIndex);
362
+ return;
363
+ }
364
+ if (e.key === 'Escape') {
365
+ e.preventDefault();
366
+ hideSuggestions();
367
+ return;
368
+ }
369
+ }
370
+ // Submit
371
+ if (e.key === 'Enter' && !e.shiftKey) {
372
+ if (options.multiline === false || !options.multiline) {
373
+ // Single-line or default: Enter submits
374
+ e.preventDefault();
375
+ const parts = parseParts();
376
+ if (parts.length > 0) {
377
+ options.onSubmit?.(parts);
378
+ }
379
+ return;
380
+ }
381
+ }
382
+ if (e.key === 'Enter' && !e.shiftKey && options.multiline) {
383
+ e.preventDefault();
384
+ const parts = parseParts();
385
+ if (parts.length > 0) {
386
+ options.onSubmit?.(parts);
387
+ }
388
+ return;
389
+ }
390
+ // Trigger detection — check the character being typed, not e.key
391
+ // (e.key can differ from the actual inserted char on some layouts)
392
+ if (triggerChars.has(e.key) && !activeTrigger) {
393
+ const trigger = triggers.find(t => t.char === e.key);
394
+ // Use setTimeout(0) instead of rAF — rAF can fire before the
395
+ // input event, causing the anchor to be captured too early
396
+ setTimeout(() => {
397
+ const sel = window.getSelection();
398
+ if (!sel || sel.rangeCount === 0)
399
+ return;
400
+ activeTrigger = trigger;
401
+ triggerAnchorNode = sel.focusNode;
402
+ triggerAnchorOffset = Math.max(0, sel.focusOffset - trigger.char.length);
403
+ }, 0);
404
+ }
405
+ // Backspace near token — handle deletion of whole token
406
+ if (e.key === 'Backspace') {
407
+ const sel = window.getSelection();
408
+ if (sel && sel.isCollapsed && sel.rangeCount > 0) {
409
+ const node = sel.focusNode;
410
+ const offset = sel.focusOffset;
411
+ // If caret is in a zero-width space right after a token
412
+ if (node && node.nodeType === Node.TEXT_NODE && offset === 0) {
413
+ const prev = node.previousSibling;
414
+ if (prev && prev instanceof HTMLElement && prev.classList.contains('tokenized-token')) {
415
+ e.preventDefault();
416
+ if (node.textContent === '\u200B')
417
+ node.remove();
418
+ prev.remove();
419
+ fireInput();
420
+ return;
421
+ }
422
+ }
423
+ // If caret is at offset 1 in a zwsp text node (common position)
424
+ if (node && node.nodeType === Node.TEXT_NODE && node.textContent === '\u200B' && offset <= 1) {
425
+ const prev = node.previousSibling;
426
+ if (prev && prev instanceof HTMLElement && prev.classList.contains('tokenized-token')) {
427
+ e.preventDefault();
428
+ node.remove();
429
+ prev.remove();
430
+ fireInput();
431
+ return;
432
+ }
433
+ }
434
+ }
435
+ }
436
+ });
437
+ editor.addEventListener('blur', () => {
438
+ // Delay to allow mousedown on suggestions
439
+ setTimeout(() => {
440
+ if (!suggestionsEl.matches(':hover')) {
441
+ hideSuggestions();
442
+ }
443
+ }, 150);
444
+ });
445
+ // Prevent dropping content into tokens
446
+ editor.addEventListener('drop', (e) => {
447
+ const target = e.target;
448
+ if (target.closest?.('.tokenized-token')) {
449
+ e.preventDefault();
450
+ }
451
+ });
452
+ // Clean pasted content — strip HTML, keep plain text
453
+ editor.addEventListener('paste', (e) => {
454
+ e.preventDefault();
455
+ const text = e.clipboardData?.getData('text/plain') ?? '';
456
+ document.execCommand('insertText', false, text);
457
+ });
458
+ // ── Public API ──
459
+ return {
460
+ getParts() {
461
+ return parseParts();
462
+ },
463
+ getText() {
464
+ return serializeText(parseParts());
465
+ },
466
+ setParts(parts) {
467
+ editor.innerHTML = '';
468
+ for (const part of parts) {
469
+ if (part.kind === 'text') {
470
+ const lines = part.text.split('\n');
471
+ for (let i = 0; i < lines.length; i++) {
472
+ if (lines[i].length > 0) {
473
+ editor.appendChild(document.createTextNode(lines[i]));
474
+ }
475
+ if (i < lines.length - 1) {
476
+ editor.appendChild(document.createElement('br'));
477
+ }
478
+ }
479
+ }
480
+ else {
481
+ editor.appendChild(buildTokenSpan(part.token));
482
+ editor.appendChild(document.createTextNode('\u200B'));
483
+ }
484
+ }
485
+ },
486
+ insertToken(token) {
487
+ insertTokenAtCaret(token);
488
+ },
489
+ clear() {
490
+ editor.innerHTML = '';
491
+ hideSuggestions();
492
+ fireInput();
493
+ },
494
+ focus() {
495
+ editor.focus();
496
+ // Drop the caret at the very end so subsequent typing / inserts land
497
+ // inside the editor.
498
+ const sel = window.getSelection();
499
+ if (!sel)
500
+ return;
501
+ const range = document.createRange();
502
+ range.selectNodeContents(editor);
503
+ range.collapse(false);
504
+ sel.removeAllRanges();
505
+ sel.addRange(range);
506
+ },
507
+ getElement() {
508
+ return root;
509
+ },
510
+ setPlaceholder(text) {
511
+ editor.dataset.placeholder = text;
512
+ }
513
+ };
514
+ }
515
+ // ─── Ready-made triggers ─────────────────────────────────────────────────────
516
+ /**
517
+ * `@` mentions. By default it searches every Fauna user; pass `people` (a
518
+ * group's members, say) to pick only from that list, shown in full as soon as
519
+ * `@` is typed.
520
+ */
521
+ export function createUserTrigger(credentials, options = {}) {
522
+ const domain = credentials.email.includes('@')
523
+ ? credentials.email.split('@')[1]
524
+ : '';
525
+ const suggestion = (p) => {
526
+ const name = p.displayName ?? p.email.split('@')[0];
527
+ const avatar = p.icon
528
+ ? p.icon
529
+ : `https://ui-avatars.com/api/?name=${encodeURIComponent(name)}&background=0B1020&color=F3F6FF&size=64`;
530
+ return {
531
+ label: name,
532
+ subtitle: p.email,
533
+ image: avatar,
534
+ data: { email: p.email, displayName: p.displayName, icon: p.icon }
535
+ };
536
+ };
537
+ return {
538
+ char: '@',
539
+ type: 'user',
540
+ async suggest(query) {
541
+ const people = options.people;
542
+ if (people) {
543
+ const needle = query.trim().toLowerCase();
544
+ return people
545
+ .filter((p) => p.email !== credentials.email)
546
+ .filter((p) => !needle
547
+ || (p.displayName ?? '').toLowerCase().includes(needle)
548
+ || p.email.toLowerCase().includes(needle))
549
+ .slice(0, 12)
550
+ .map(suggestion);
551
+ }
552
+ if (!query.trim())
553
+ return [];
554
+ try {
555
+ // If query has no @, append caller's domain to email search
556
+ // so same-domain users rank higher server-side
557
+ const emailQuery = query.includes('@') ? query : (query + '@' + domain);
558
+ const results = await Fauna.Authentification.query(credentials, {
559
+ displayName: query,
560
+ email: emailQuery
561
+ });
562
+ return results.slice(0, 12).map(suggestion);
563
+ }
564
+ catch {
565
+ return [];
566
+ }
567
+ },
568
+ onPick(s) {
569
+ return {
570
+ type: 'user',
571
+ value: s.data?.email ?? s.label,
572
+ label: s.label,
573
+ icon: 'person'
574
+ };
575
+ }
576
+ };
577
+ }
578
+ export function createDriveTrigger(credentials) {
579
+ return {
580
+ char: '@',
581
+ type: 'drive',
582
+ async suggest(query) {
583
+ // Normalize query path
584
+ const path = query.replace(/\\/g, '/');
585
+ try {
586
+ // List the parent directory
587
+ const parentDir = path.includes('/')
588
+ ? path.slice(0, path.lastIndexOf('/') + 1)
589
+ : '';
590
+ const prefix = path.slice(parentDir.length).toLowerCase();
591
+ const entries = await Fauna.Drive.list(credentials, parentDir);
592
+ return entries
593
+ .filter(entry => {
594
+ const name = entry.split('/').filter(Boolean).pop() ?? entry;
595
+ return name.toLowerCase().startsWith(prefix) || prefix === '';
596
+ })
597
+ .slice(0, 12)
598
+ .map(entry => {
599
+ const isDir = entry.endsWith('/');
600
+ const name = entry.split('/').filter(Boolean).pop() ?? entry;
601
+ return {
602
+ label: name,
603
+ subtitle: parentDir + entry,
604
+ icon: isDir ? 'folder' : 'description',
605
+ data: { path: parentDir + entry, isDir }
606
+ };
607
+ });
608
+ }
609
+ catch {
610
+ return [];
611
+ }
612
+ },
613
+ onPick(s) {
614
+ return {
615
+ type: 'drive',
616
+ value: s.data?.path ?? s.label,
617
+ label: s.label,
618
+ icon: s.icon ?? 'description'
619
+ };
620
+ }
621
+ };
622
+ }