fauna-api 0.0.0-stage → 1.0.679

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 +108 -0
  42. package/dist/src/extensions/web/session.js +470 -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 +87 -4
  171. package/README.md +0 -3
@@ -0,0 +1,36 @@
1
+ /** The live room a tool opened, to watch it: a browser, or a machine's desktop, by access code. */
2
+ export type ToolSession = {
3
+ kind: 'browser' | 'desktop';
4
+ code: string;
5
+ url?: string;
6
+ };
7
+ export type ChatSequenceToolCall = {
8
+ id?: string;
9
+ name: string;
10
+ args: Record<string, unknown>;
11
+ status?: 'running' | 'success' | 'error';
12
+ result?: string;
13
+ progress?: unknown;
14
+ session?: ToolSession;
15
+ };
16
+ /** A file in a message: its type and name, and the FILE part it came in (ref, access code) for opening it. */
17
+ export type ChatFile = {
18
+ mime: string;
19
+ name: string;
20
+ part?: any;
21
+ };
22
+ export type ChatMessageBlock = {
23
+ type: 'text';
24
+ text: string;
25
+ } | {
26
+ type: 'tool';
27
+ call: ChatSequenceToolCall;
28
+ } | ({
29
+ type: 'file';
30
+ } & ChatFile);
31
+ export declare function roomPartsToChatFields(parts: any[]): {
32
+ text: string;
33
+ files?: ChatFile[];
34
+ toolCalls?: ChatSequenceToolCall[];
35
+ sequence?: ChatMessageBlock[];
36
+ };
@@ -0,0 +1,42 @@
1
+ function toolSession(raw) {
2
+ if (!raw || typeof raw.code !== 'string' || !raw.code)
3
+ return undefined;
4
+ return { kind: raw.kind === 'desktop' ? 'desktop' : 'browser', code: raw.code, ...(typeof raw.url === 'string' && raw.url ? { url: raw.url } : {}) };
5
+ }
6
+ export function roomPartsToChatFields(parts) {
7
+ const texts = [];
8
+ const files = [];
9
+ const toolCalls = [];
10
+ const sequence = [];
11
+ for (const part of parts ?? []) {
12
+ if (part?.type === 'TEXT' && typeof part.text === 'string') {
13
+ texts.push(part.text);
14
+ if (part.text.trim())
15
+ sequence.push({ type: 'text', text: part.text });
16
+ }
17
+ else if (part?.type === 'FILE') {
18
+ const file = { mime: part.contentType ?? '', name: part.name ?? part.ref ?? 'file', part };
19
+ files.push(file);
20
+ sequence.push({ type: 'file', ...file });
21
+ }
22
+ else if (part?.type === 'INVOCATION') {
23
+ const session = toolSession(part.session);
24
+ const call = {
25
+ id: part.id,
26
+ name: part.name,
27
+ args: part.args ?? {},
28
+ status: part.status,
29
+ result: part.result,
30
+ ...(session ? { session } : {}),
31
+ };
32
+ toolCalls.push(call);
33
+ sequence.push({ type: 'tool', call });
34
+ }
35
+ }
36
+ return {
37
+ text: texts.join('\n').trim(),
38
+ files: files.length > 0 ? files : undefined,
39
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
40
+ sequence: sequence.length > 0 ? sequence : undefined,
41
+ };
42
+ }
@@ -0,0 +1,19 @@
1
+ export type CrownTheme = Partial<{
2
+ gold: string;
3
+ charcoal: string;
4
+ silver: string;
5
+ }>;
6
+ export type MountCrownOptions = CrownTheme & {
7
+ inset?: number;
8
+ host?: HTMLElement | null;
9
+ mode?: "hover" | "loop";
10
+ duration?: number;
11
+ };
12
+ export declare function renderCrown(context: CanvasRenderingContext2D, size: number, t?: number, theme?: CrownTheme): void;
13
+ export declare function drawCrown(size: number, theme?: CrownTheme): HTMLImageElement;
14
+ export declare function getFaunaLogoDataUrl(): string;
15
+ export declare function mountCrown(icon: HTMLCanvasElement, options?: MountCrownOptions): () => void;
16
+ /** Fills a link with the Powered by Fauna badge: the crown on its charcoal tile (`brands.css`), turning when the badge is hovered. */
17
+ export declare function mountPoweredBy(link: HTMLAnchorElement, options?: CrownTheme & {
18
+ inset?: number;
19
+ }): () => void;
@@ -0,0 +1,199 @@
1
+ const GOLD = "rgb(247, 239, 138)";
2
+ const CHARCOAL = "rgb(21, 21, 21)";
3
+ const SILVER = "rgb(192, 192, 192)";
4
+ function lerp(a, b, t) {
5
+ return a + (b - a) * t;
6
+ }
7
+ function parseColor(value, fallback) {
8
+ if (!value)
9
+ return fallback;
10
+ const hex = value.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
11
+ if (hex) {
12
+ const raw = hex[1];
13
+ const full = raw.length === 3 ? raw.split("").map((c) => c + c).join("") : raw;
14
+ return [
15
+ parseInt(full.slice(0, 2), 16),
16
+ parseInt(full.slice(2, 4), 16),
17
+ parseInt(full.slice(4, 6), 16),
18
+ ];
19
+ }
20
+ const rgb = value.match(/\d+/g)?.map((c) => parseInt(c));
21
+ if (rgb && rgb.length >= 3)
22
+ return [rgb[0], rgb[1], rgb[2]];
23
+ return fallback;
24
+ }
25
+ function lerpRGB(a, b, t) {
26
+ const aRGB = parseColor(a, [21, 21, 21]);
27
+ const bRGB = parseColor(b, [247, 239, 138]);
28
+ return `rgb(${lerp(aRGB[0], bRGB[0], t)}, ${lerp(aRGB[1], bRGB[1], t)}, ${lerp(aRGB[2], bRGB[2], t)})`;
29
+ }
30
+ function sineInOut(x) {
31
+ return -(Math.cos(Math.PI * x) - 1) / 2;
32
+ }
33
+ export function renderCrown(context, size, t = Math.PI / 2, theme = {}) {
34
+ const gold = theme.gold ?? GOLD;
35
+ const charcoal = theme.charcoal ?? CHARCOAL;
36
+ const silver = theme.silver ?? SILVER;
37
+ const bars = 35;
38
+ const fov = 1.5;
39
+ const sagitta = size * (0.12 + 0.12 * Math.sin(t));
40
+ const barWidth = size / (fov ** 2 * bars);
41
+ const barHeight = size / 2;
42
+ const barAngles = (Math.PI * 2) / bars;
43
+ const radius = sagitta / 2 + size ** 2 / (8 * sagitta);
44
+ const bottomMidPoint = [size / 2, size / 2 + barHeight / 4];
45
+ const bottomApex = [bottomMidPoint[0], bottomMidPoint[1] - sagitta];
46
+ const bottomCenter = [bottomApex[0], bottomApex[1] + radius];
47
+ const ease = (v) => Math.sign(v) * Math.pow(Math.abs(v), 0.8);
48
+ const spikeMultiplier = (i) => 1 + 0.4 * Math.sin(2 * Math.PI * t + (i % 2) * Math.PI);
49
+ context.save();
50
+ context.beginPath();
51
+ context.arc(bottomCenter[0], bottomCenter[1], radius, 0, Math.PI * 2);
52
+ context.clip();
53
+ for (let i = 0; i < bars; i++) {
54
+ const angle = barAngles * i;
55
+ const raw = Math.sin(angle - t);
56
+ if (raw > 0)
57
+ continue;
58
+ const depth = ease(raw);
59
+ const spikeHeight = spikeMultiplier(i);
60
+ const x = (Math.cos(angle - t) * size) / 2 + size / 2;
61
+ const y = -sagitta * depth * spikeHeight;
62
+ const width = lerp(0, barWidth / fov, -depth);
63
+ context.fillStyle = lerpRGB(charcoal, gold, ((-depth + 1) / 2) * 0.5 + 0.5);
64
+ context.globalAlpha = lerp(0.4, 0.8, -depth);
65
+ context.beginPath();
66
+ context.roundRect(x - width / 2, size / 2 - barHeight / 2 + y, width, barHeight, width / 2);
67
+ context.fill();
68
+ }
69
+ context.restore();
70
+ context.save();
71
+ context.beginPath();
72
+ // Wider than the crown's box: spikes overshoot it mid turn and must reach into any inset.
73
+ context.rect(-size, -size, size * 3, size * 3);
74
+ context.arc(bottomCenter[0], bottomCenter[1], radius, 0, Math.PI * 2);
75
+ context.clip("evenodd");
76
+ for (let i = 0; i < bars; i++) {
77
+ const angle = barAngles * i;
78
+ const raw = Math.sin(angle - t);
79
+ if (raw <= 0)
80
+ continue;
81
+ const depth = ease(raw);
82
+ const spikeHeight = spikeMultiplier(i);
83
+ const x = (Math.cos(angle - t) * size) / 2 + size / 2;
84
+ const y = -sagitta * depth * spikeHeight;
85
+ const width = lerp(0, fov * barWidth, depth);
86
+ context.fillStyle = lerpRGB(charcoal, gold, ((depth + 1) / 2) * 0.5 + 0.5);
87
+ context.globalAlpha = lerp(0.7, 1, depth);
88
+ context.beginPath();
89
+ context.roundRect(x - width / 2, size / 2 - barHeight / 2 + y, width, barHeight, width / 2);
90
+ context.fill();
91
+ }
92
+ context.restore();
93
+ context.save();
94
+ context.strokeStyle = silver;
95
+ context.globalAlpha = 0.2;
96
+ context.lineWidth = size / 200;
97
+ context.beginPath();
98
+ context.arc(bottomCenter[0], bottomCenter[1], radius, 0, Math.PI * 2);
99
+ context.stroke();
100
+ context.restore();
101
+ }
102
+ export function drawCrown(size, theme = {}) {
103
+ const canvas = document.createElement("canvas");
104
+ canvas.width = size;
105
+ canvas.height = size;
106
+ const context = canvas.getContext("2d");
107
+ context.fillStyle = theme.charcoal ?? CHARCOAL;
108
+ context.fillRect(0, 0, size, size);
109
+ renderCrown(context, size, Math.PI / 2, theme);
110
+ const image = new Image(size, size);
111
+ image.src = canvas.toDataURL();
112
+ return image;
113
+ }
114
+ let logoDataUrl = null;
115
+ export function getFaunaLogoDataUrl() {
116
+ if (!logoDataUrl)
117
+ logoDataUrl = drawCrown(192).src;
118
+ return logoDataUrl;
119
+ }
120
+ export function mountCrown(icon, options = {}) {
121
+ const dpr = window.devicePixelRatio || 1;
122
+ const mode = options.mode ?? "hover";
123
+ const duration = options.duration ?? (mode === "loop" ? 24000 : 2800);
124
+ const inset = Math.max(0, Math.min(0.35, options.inset ?? 0));
125
+ const theme = {
126
+ gold: options.gold,
127
+ charcoal: options.charcoal,
128
+ silver: options.silver,
129
+ };
130
+ const host = options.host ?? icon.parentElement ?? icon;
131
+ const draw = (t = Math.PI / 2) => {
132
+ const cssSize = icon.getBoundingClientRect().width;
133
+ if (cssSize <= 0)
134
+ return;
135
+ icon.width = cssSize * dpr;
136
+ icon.height = cssSize * dpr;
137
+ const context = icon.getContext("2d");
138
+ if (!context)
139
+ return;
140
+ context.save();
141
+ context.scale(dpr, dpr);
142
+ context.clearRect(0, 0, cssSize, cssSize);
143
+ const pad = cssSize * inset;
144
+ const drawSize = cssSize - pad * 2;
145
+ context.translate(pad, pad);
146
+ renderCrown(context, drawSize, t, theme);
147
+ context.restore();
148
+ };
149
+ draw();
150
+ const onResize = () => draw();
151
+ window.addEventListener("resize", onResize);
152
+ let raf = 0;
153
+ const stop = () => {
154
+ cancelAnimationFrame(raf);
155
+ raf = 0;
156
+ };
157
+ const onEnter = () => {
158
+ if (mode !== "hover")
159
+ return;
160
+ stop();
161
+ const start = performance.now();
162
+ const step = (now) => {
163
+ const p = Math.min((now - start) / duration, 1);
164
+ draw(2 * Math.PI * sineInOut(p) + Math.PI / 2);
165
+ if (p < 1)
166
+ raf = requestAnimationFrame(step);
167
+ };
168
+ raf = requestAnimationFrame(step);
169
+ };
170
+ if (mode === "hover")
171
+ host.addEventListener("mouseenter", onEnter);
172
+ if (mode === "loop") {
173
+ let last = performance.now();
174
+ let t = Math.PI / 2;
175
+ const step = (now) => {
176
+ t += ((now - last) / duration) * Math.PI * 2;
177
+ last = now;
178
+ draw(t);
179
+ raf = requestAnimationFrame(step);
180
+ };
181
+ raf = requestAnimationFrame(step);
182
+ }
183
+ return () => {
184
+ stop();
185
+ window.removeEventListener("resize", onResize);
186
+ host.removeEventListener("mouseenter", onEnter);
187
+ };
188
+ }
189
+ /** Fills a link with the Powered by Fauna badge: the crown on its charcoal tile (`brands.css`), turning when the badge is hovered. */
190
+ export function mountPoweredBy(link, options = {}) {
191
+ link.href = "https://faunakit.com";
192
+ const crown = document.createElement("canvas");
193
+ crown.className = "fauna-powered-crown";
194
+ crown.setAttribute("aria-hidden", "true");
195
+ const label = document.createElement("span");
196
+ label.textContent = "Powered by Fauna";
197
+ link.replaceChildren(crown, label);
198
+ return mountCrown(crown, { inset: 0.14, host: link, ...options });
199
+ }
@@ -0,0 +1,73 @@
1
+ import React from "react";
2
+ import type { Presence as Seat } from "../shared/server.js";
3
+ /** FNV-1a style hash so the same seed always maps to the same color. */
4
+ export declare function hashSeed(seed: string): number;
5
+ /**
6
+ * Vibrant (high-sat, mid-light) HSL color deterministically derived from `seed`.
7
+ */
8
+ export declare function colorFromSeed(seed: string): string;
9
+ /** Pin / chip color seed. Email first so composer drafts match sent history. */
10
+ export declare function authorColorSeed(author?: {
11
+ id?: string;
12
+ profile?: {
13
+ email?: string;
14
+ };
15
+ }): string | undefined;
16
+ /** Filled Material-style push pin. Color via CSS `currentColor`. */
17
+ export declare function PinMark({ className, style }: {
18
+ className?: string;
19
+ style?: React.CSSProperties;
20
+ }): React.JSX.Element;
21
+ export type CursorProps = {
22
+ /** Stable id (presence id, user id, email, …) — seeds the color. */
23
+ seed: string;
24
+ /** Optional label under / beside the tip (display name). */
25
+ label?: string;
26
+ /** A CSS cursor keyword (`pointer`, `text`, `nwse-resize`...). Unknown ones draw the arrow. */
27
+ shape?: string;
28
+ className?: string;
29
+ style?: React.CSSProperties;
30
+ };
31
+ /**
32
+ * Returns a React SVG cursor whose fill is a vibrant color seeded by `seed`, drawn in
33
+ * `shape`. Its hotspot is at (0, 0) of the element's box — position with
34
+ * `style={{ left, top }}` (or transform) at the client's x/y.
35
+ */
36
+ export declare function Cursor({ seed, label, shape, className, style }: CursorProps): React.JSX.Element;
37
+ /** Convenience: seed → reusable cursor component with that color baked in. */
38
+ export declare function cursorFor(seed: string): React.FC<Omit<CursorProps, "seed">>;
39
+ export type PresenceProps = {
40
+ /** Where this seat's pointer moves, like a room's input layer. */
41
+ layer: HTMLElement | null;
42
+ self: Seat | null;
43
+ peers: Seat[];
44
+ /** Page coordinates for a client point, or null outside the page. */
45
+ toPage: (clientX: number, clientY: number) => {
46
+ x: number;
47
+ y: number;
48
+ } | null;
49
+ /** Where a page point sits in this container. */
50
+ toScreen: (x: number, y: number) => {
51
+ x: number;
52
+ y: number;
53
+ };
54
+ /** The room's real pointer and the seat driving it. That seat is drawn where the
55
+ * pointer really is, in its shape, instead of where its presence says. */
56
+ machine?: {
57
+ seat: string;
58
+ x: number;
59
+ y: number;
60
+ shape?: string;
61
+ } | null;
62
+ };
63
+ /**
64
+ * Share this seat's pointer in page coordinates and draw everyone else's at this
65
+ * view's scale, so a cursor lands on the same spot however each person sized
66
+ * their window. Put it in a positioned container over the page.
67
+ */
68
+ export declare function Presence({ layer, self, peers, toPage, toScreen, machine }: PresenceProps): React.JSX.Element;
69
+ /** Presence cursors for apps without React. Call `update` as peers move or the view resizes. */
70
+ export declare function mountPresence(el: HTMLElement, props: PresenceProps): {
71
+ update(props: PresenceProps): void;
72
+ destroy(): void;
73
+ };
@@ -0,0 +1,175 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ /** FNV-1a style hash so the same seed always maps to the same color. */
5
+ export function hashSeed(seed) {
6
+ let h = 2166136261;
7
+ for (let i = 0; i < seed.length; i++) {
8
+ h ^= seed.charCodeAt(i);
9
+ h = Math.imul(h, 16777619);
10
+ }
11
+ return h >>> 0;
12
+ }
13
+ /**
14
+ * Vibrant (high-sat, mid-light) HSL color deterministically derived from `seed`.
15
+ */
16
+ export function colorFromSeed(seed) {
17
+ const h = hashSeed(seed || "?");
18
+ const hue = h % 360;
19
+ // Keep sat/light in a punchy band so cursors stay readable on light/dark UI.
20
+ const sat = 68 + (h >>> 8) % 22; // 68–89%
21
+ const light = 48 + (h >>> 16) % 14; // 48–61%
22
+ return `hsl(${hue} ${sat}% ${light}%)`;
23
+ }
24
+ /** Pin / chip color seed. Email first so composer drafts match sent history. */
25
+ export function authorColorSeed(author) {
26
+ const email = author?.profile?.email?.trim();
27
+ if (email)
28
+ return email;
29
+ const id = author?.id?.trim();
30
+ return id || undefined;
31
+ }
32
+ /** Filled Material-style push pin. Color via CSS `currentColor`. */
33
+ export function PinMark({ className, style }) {
34
+ return (_jsx("svg", { className: className, width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", style: {
35
+ display: "block",
36
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.45))",
37
+ overflow: "visible",
38
+ ...style,
39
+ }, "aria-hidden": true, children: _jsx("path", { d: "M16 9V4h1c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1h1v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z", fill: "currentColor", stroke: "rgba(255,255,255,0.95)", strokeWidth: "2.1", strokeLinejoin: "round", strokeLinecap: "round", style: { paintOrder: "stroke fill" } }) }));
40
+ }
41
+ const ARROW = { d: "M4 2.5v16.2l4.1-3.5 2.4 5.8 2.5-1-2.4-5.7H17L4 2.5z", hotspot: [4, 2.5] };
42
+ const RESIZE_EW = "M2.5 12l4-4v2.5h11V8l4 4-4 4v-2.5h-11V16z";
43
+ const GLYPHS = {
44
+ pointer: {
45
+ d: "M9 3.5a1.5 1.5 0 0 1 3 0V10h.5V8.5a1.5 1.5 0 0 1 3 0V10h.5V9.5a1.5 1.5 0 0 1 3 0V11h.3a1.5 1.5 0 0 1 1.7 1.5V16c0 3-2.2 5.5-5.2 5.5H12c-1.6 0-3-.8-3.9-2.1L5.2 15a1.6 1.6 0 0 1 2.4-2L9 14.4V3.5z",
46
+ hotspot: [10.5, 2],
47
+ },
48
+ grab: {
49
+ d: "M8 9V6.5a1.5 1.5 0 0 1 3 0V9h.5V5a1.5 1.5 0 0 1 3 0v4h.5V6a1.5 1.5 0 0 1 3 0v4.5h.2V9a1.5 1.5 0 0 1 3 0v6.5c0 3.3-2.5 6-5.8 6h-2.2c-1.7 0-3.3-.9-4.2-2.3L4.6 15a1.5 1.5 0 0 1 2.5-1.7L8 14.5V9z",
50
+ hotspot: [13, 12],
51
+ },
52
+ grabbing: {
53
+ d: "M7.5 11a1.5 1.5 0 0 1 3 0v-.5a1.5 1.5 0 0 1 3 0v.5a1.5 1.5 0 0 1 3 0v.5a1.5 1.5 0 0 1 3 0v4c0 3.3-2.5 6-5.8 6h-2c-1.7 0-3.2-.8-4.1-2.2L5 15.5a1.5 1.5 0 0 1 2.5-1.6V11z",
54
+ hotspot: [13, 13],
55
+ },
56
+ text: { d: "M9 3.5h6M12 3.5v17M9 20.5h6", hotspot: [12, 12], lines: true },
57
+ move: {
58
+ d: "M12 2.5l3 3.2h-2v5.3h5.3v-2l3.2 3-3.2 3v-2H13v5.3h2l-3 3.2-3-3.2h2V13H5.7v2L2.5 12l3.2-3v2H11V5.7H9z",
59
+ hotspot: [12, 12],
60
+ },
61
+ "ew-resize": { d: RESIZE_EW, hotspot: [12, 12] },
62
+ "ns-resize": { d: RESIZE_EW, hotspot: [12, 12], rotate: 90 },
63
+ "nwse-resize": { d: RESIZE_EW, hotspot: [12, 12], rotate: 45 },
64
+ "nesw-resize": { d: RESIZE_EW, hotspot: [12, 12], rotate: -45 },
65
+ "not-allowed": { d: "M12 4.5a7.5 7.5 0 1 0 0 15 7.5 7.5 0 0 0 0-15zM6.7 6.7l10.6 10.6", hotspot: [12, 12], lines: true },
66
+ crosshair: { d: "M12 3v18M3 12h18", hotspot: [12, 12], lines: true },
67
+ };
68
+ const ALIASES = {
69
+ "vertical-text": "text",
70
+ "all-scroll": "move",
71
+ "e-resize": "ew-resize", "w-resize": "ew-resize", "col-resize": "ew-resize",
72
+ "n-resize": "ns-resize", "s-resize": "ns-resize", "row-resize": "ns-resize",
73
+ "nw-resize": "nwse-resize", "se-resize": "nwse-resize",
74
+ "ne-resize": "nesw-resize", "sw-resize": "nesw-resize",
75
+ "no-drop": "not-allowed",
76
+ cell: "crosshair",
77
+ };
78
+ function glyphFor(shape) {
79
+ if (!shape)
80
+ return ARROW;
81
+ return GLYPHS[ALIASES[shape] ?? shape] ?? ARROW;
82
+ }
83
+ /**
84
+ * Returns a React SVG cursor whose fill is a vibrant color seeded by `seed`, drawn in
85
+ * `shape`. Its hotspot is at (0, 0) of the element's box — position with
86
+ * `style={{ left, top }}` (or transform) at the client's x/y.
87
+ */
88
+ export function Cursor({ seed, label, shape, className, style }) {
89
+ const color = colorFromSeed(seed);
90
+ const glyph = glyphFor(shape);
91
+ const [hx, hy] = glyph.hotspot;
92
+ const turn = glyph.rotate ? `rotate(${glyph.rotate} 12 12)` : undefined;
93
+ return (_jsxs("div", { className: className, style: {
94
+ position: "absolute",
95
+ left: 0,
96
+ top: 0,
97
+ width: 0,
98
+ height: 0,
99
+ pointerEvents: "none",
100
+ zIndex: 40,
101
+ ...style,
102
+ }, "aria-hidden": true, children: [_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", style: {
103
+ display: "block",
104
+ // Shift so the glyph's hotspot is element (0,0).
105
+ transform: `translate(${-hx}px, ${-hy}px)`,
106
+ filter: "drop-shadow(0 1px 2px rgba(0,0,0,0.35))",
107
+ overflow: "visible",
108
+ }, children: glyph.lines ? (_jsxs("g", { transform: turn, strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: glyph.d, stroke: "rgba(255,255,255,0.92)", strokeWidth: "4.5" }), _jsx("path", { d: glyph.d, stroke: color, strokeWidth: "2" })] })) : (_jsx("path", { d: glyph.d, transform: turn, fill: color, stroke: "rgba(255,255,255,0.92)", strokeWidth: "1.25", strokeLinejoin: "round" })) }), label ? (_jsx("span", { style: {
109
+ position: "absolute",
110
+ left: 16,
111
+ top: 18,
112
+ padding: "2px 6px",
113
+ borderRadius: 6,
114
+ background: color,
115
+ color: "#fff",
116
+ fontFamily: "Inter, system-ui, sans-serif",
117
+ fontSize: 11,
118
+ fontWeight: 600,
119
+ lineHeight: 1.3,
120
+ whiteSpace: "nowrap",
121
+ boxShadow: "0 1px 3px rgba(0,0,0,0.25)",
122
+ }, children: label })) : null] }));
123
+ }
124
+ /** Convenience: seed → reusable cursor component with that color baked in. */
125
+ export function cursorFor(seed) {
126
+ return function SeededCursor(props) {
127
+ return _jsx(Cursor, { seed: seed, ...props });
128
+ };
129
+ }
130
+ /**
131
+ * Share this seat's pointer in page coordinates and draw everyone else's at this
132
+ * view's scale, so a cursor lands on the same spot however each person sized
133
+ * their window. Put it in a positioned container over the page.
134
+ */
135
+ export function Presence({ layer, self, peers, toPage, toScreen, machine }) {
136
+ const lastSent = useRef(0);
137
+ const toPageRef = useRef(toPage);
138
+ toPageRef.current = toPage;
139
+ useEffect(() => {
140
+ if (!layer || !self)
141
+ return;
142
+ const onMove = (event) => {
143
+ const now = performance.now();
144
+ if (now - lastSent.current < 40)
145
+ return;
146
+ const point = toPageRef.current(event.clientX, event.clientY);
147
+ if (!point)
148
+ return;
149
+ lastSent.current = now;
150
+ self.move([point.x, point.y, 0]);
151
+ };
152
+ layer.addEventListener("pointermove", onMove, { passive: true });
153
+ return () => layer.removeEventListener("pointermove", onMove);
154
+ }, [layer, self]);
155
+ // Id only: the same person twice is two seats, and both stay visible.
156
+ const others = self ? peers.filter((p) => p.id !== self.id) : peers;
157
+ return (_jsx("div", { className: "fauna-cursors", "aria-hidden": "true", children: others.map((peer) => {
158
+ const driving = machine?.seat === peer.id ? machine : null;
159
+ const [x, y] = driving ? [driving.x, driving.y] : peer.position;
160
+ if (!x && !y)
161
+ return null;
162
+ const at = toScreen(x, y);
163
+ const label = peer.profile?.displayName || peer.profile?.email?.split("@")[0] || "Guest";
164
+ return _jsx(Cursor, { seed: peer.id, label: label, shape: driving?.shape, style: { transform: `translate3d(${at.x}px, ${at.y}px, 0)` } }, peer.id);
165
+ }) }));
166
+ }
167
+ /** Presence cursors for apps without React. Call `update` as peers move or the view resizes. */
168
+ export function mountPresence(el, props) {
169
+ const root = createRoot(el);
170
+ root.render(_jsx(Presence, { ...props }));
171
+ return {
172
+ update(next) { root.render(_jsx(Presence, { ...next })); },
173
+ destroy() { root.unmount(); },
174
+ };
175
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Lightweight device-class heuristics for UI layout (phone vs computer).
3
+ * Pure functions over optional overrides so tests don't need a real window.
4
+ */
5
+ export type DeviceClass = 'phone' | 'computer';
6
+ export type DeviceHints = {
7
+ /** CSS viewport width in CSS pixels. */
8
+ width?: number;
9
+ /** CSS viewport height in CSS pixels. */
10
+ height?: number;
11
+ /** navigator.userAgent / userAgentData brands string. */
12
+ userAgent?: string;
13
+ /** navigator.maxTouchPoints */
14
+ maxTouchPoints?: number;
15
+ /** matchMedia('(pointer: coarse)') */
16
+ coarsePointer?: boolean;
17
+ /** matchMedia('(hover: hover)') — true when a fine hover pointer exists. */
18
+ canHover?: boolean;
19
+ };
20
+ /**
21
+ * Classify the current (or provided) environment as phone vs computer.
22
+ *
23
+ * Heuristic (ordered):
24
+ * 1. Strong UA mobile tokens → phone
25
+ * 2. Narrow viewport (≤ 768) + coarse pointer / touch → phone
26
+ * 3. Narrow viewport + no hover + touch → phone
27
+ * 4. Otherwise computer
28
+ */
29
+ export declare function detectDevice(hints?: DeviceHints): DeviceClass;
30
+ /** Convenience: true when `detectDevice` says phone. */
31
+ export declare function isPhone(hints?: DeviceHints): boolean;
32
+ /** Convenience: true when `detectDevice` says computer. */
33
+ export declare function isComputer(hints?: DeviceHints): boolean;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Lightweight device-class heuristics for UI layout (phone vs computer).
3
+ * Pure functions over optional overrides so tests don't need a real window.
4
+ */
5
+ const PHONE_UA = /Android.+Mobile|iPhone|iPod|Windows Phone|webOS|BlackBerry|IEMobile|Opera Mini|Mobile Safari|Mobile\/\w+/i;
6
+ /**
7
+ * Classify the current (or provided) environment as phone vs computer.
8
+ *
9
+ * Heuristic (ordered):
10
+ * 1. Strong UA mobile tokens → phone
11
+ * 2. Narrow viewport (≤ 768) + coarse pointer / touch → phone
12
+ * 3. Narrow viewport + no hover + touch → phone
13
+ * 4. Otherwise computer
14
+ */
15
+ export function detectDevice(hints = {}) {
16
+ const width = hints.width ??
17
+ (typeof window !== 'undefined' ? window.innerWidth : undefined);
18
+ const userAgent = hints.userAgent ??
19
+ (typeof navigator !== 'undefined' ? navigator.userAgent : '');
20
+ const maxTouchPoints = hints.maxTouchPoints ??
21
+ (typeof navigator !== 'undefined' ? navigator.maxTouchPoints ?? 0 : 0);
22
+ let coarsePointer = hints.coarsePointer;
23
+ let canHover = hints.canHover;
24
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
25
+ if (coarsePointer === undefined) {
26
+ coarsePointer = window.matchMedia('(pointer: coarse)').matches;
27
+ }
28
+ if (canHover === undefined) {
29
+ canHover = window.matchMedia('(hover: hover)').matches;
30
+ }
31
+ }
32
+ if (userAgent && PHONE_UA.test(userAgent))
33
+ return 'phone';
34
+ const narrow = width !== undefined && width <= 768;
35
+ const touchy = maxTouchPoints > 0 || coarsePointer === true;
36
+ if (narrow && touchy)
37
+ return 'phone';
38
+ if (narrow && canHover === false && maxTouchPoints > 0)
39
+ return 'phone';
40
+ return 'computer';
41
+ }
42
+ /** Convenience: true when `detectDevice` says phone. */
43
+ export function isPhone(hints) {
44
+ return detectDevice(hints) === 'phone';
45
+ }
46
+ /** Convenience: true when `detectDevice` says computer. */
47
+ export function isComputer(hints) {
48
+ return detectDevice(hints) === 'computer';
49
+ }