@fluxy-chat/create-fluxy-chat 0.5.9 → 0.5.13

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 (152) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/index.js +111 -1
  3. package/package.json +1 -1
  4. package/readme.md +11 -0
  5. package/templates/basic/package.json +1 -1
  6. package/templates/comments-board/.env.example +11 -0
  7. package/templates/comments-board/README.md +9 -0
  8. package/templates/comments-board/index.html +12 -0
  9. package/templates/comments-board/package.json +26 -0
  10. package/templates/comments-board/src/App.tsx +86 -0
  11. package/templates/comments-board/src/index.css +75 -0
  12. package/templates/comments-board/src/main.tsx +10 -0
  13. package/templates/comments-board/src/session.ts +73 -0
  14. package/templates/comments-board/src/vite-env.d.ts +13 -0
  15. package/templates/comments-board/tsconfig.json +21 -0
  16. package/templates/comments-board/vite.config.ts +7 -0
  17. package/templates/deal-room/.env.example +11 -0
  18. package/templates/deal-room/README.md +11 -0
  19. package/templates/deal-room/index.html +12 -0
  20. package/templates/deal-room/package.json +25 -0
  21. package/templates/deal-room/src/App.tsx +110 -0
  22. package/templates/deal-room/src/index.css +75 -0
  23. package/templates/deal-room/src/main.tsx +10 -0
  24. package/templates/deal-room/src/session.ts +73 -0
  25. package/templates/deal-room/src/vite-env.d.ts +13 -0
  26. package/templates/deal-room/tsconfig.json +21 -0
  27. package/templates/deal-room/vite.config.ts +7 -0
  28. package/templates/discord/package.json +1 -1
  29. package/templates/draw/.env.example +7 -0
  30. package/templates/draw/README.md +7 -0
  31. package/templates/draw/index.html +12 -0
  32. package/templates/draw/package.json +25 -0
  33. package/templates/draw/src/App.tsx +91 -0
  34. package/templates/draw/src/index.css +75 -0
  35. package/templates/draw/src/main.tsx +10 -0
  36. package/templates/draw/src/session.ts +73 -0
  37. package/templates/draw/src/vite-env.d.ts +13 -0
  38. package/templates/draw/tsconfig.json +21 -0
  39. package/templates/draw/vite.config.ts +7 -0
  40. package/templates/fleet-panel/.env.example +11 -0
  41. package/templates/fleet-panel/README.md +9 -0
  42. package/templates/fleet-panel/index.html +12 -0
  43. package/templates/fleet-panel/package.json +25 -0
  44. package/templates/fleet-panel/src/App.tsx +83 -0
  45. package/templates/fleet-panel/src/index.css +75 -0
  46. package/templates/fleet-panel/src/main.tsx +10 -0
  47. package/templates/fleet-panel/src/session.ts +73 -0
  48. package/templates/fleet-panel/src/vite-env.d.ts +13 -0
  49. package/templates/fleet-panel/tsconfig.json +21 -0
  50. package/templates/fleet-panel/vite.config.ts +7 -0
  51. package/templates/full/package.json +4 -4
  52. package/templates/full/scripts/hoist-fluxy-sdk.mjs +1 -1
  53. package/templates/full/src/App.tsx +20 -6
  54. package/templates/game-tick/.env.example +11 -0
  55. package/templates/game-tick/README.md +9 -0
  56. package/templates/game-tick/index.html +12 -0
  57. package/templates/game-tick/package.json +25 -0
  58. package/templates/game-tick/src/App.tsx +87 -0
  59. package/templates/game-tick/src/index.css +75 -0
  60. package/templates/game-tick/src/main.tsx +10 -0
  61. package/templates/game-tick/src/session.ts +73 -0
  62. package/templates/game-tick/src/vite-env.d.ts +13 -0
  63. package/templates/game-tick/tsconfig.json +21 -0
  64. package/templates/game-tick/vite.config.ts +7 -0
  65. package/templates/iot-panel/.env.example +7 -0
  66. package/templates/iot-panel/README.md +16 -0
  67. package/templates/iot-panel/index.html +12 -0
  68. package/templates/iot-panel/package.json +25 -0
  69. package/templates/iot-panel/src/App.tsx +97 -0
  70. package/templates/iot-panel/src/index.css +75 -0
  71. package/templates/iot-panel/src/main.tsx +10 -0
  72. package/templates/iot-panel/src/session.ts +73 -0
  73. package/templates/iot-panel/src/vite-env.d.ts +13 -0
  74. package/templates/iot-panel/tsconfig.json +21 -0
  75. package/templates/iot-panel/vite.config.ts +7 -0
  76. package/templates/javascript-live-cursors/.env.example +9 -0
  77. package/templates/javascript-live-cursors/README.md +9 -0
  78. package/templates/javascript-live-cursors/index.html +17 -0
  79. package/templates/javascript-live-cursors/package.json +18 -0
  80. package/templates/javascript-live-cursors/src/index.css +31 -0
  81. package/templates/javascript-live-cursors/src/main.ts +89 -0
  82. package/templates/javascript-live-cursors/src/vite-env.d.ts +12 -0
  83. package/templates/javascript-live-cursors/tsconfig.json +13 -0
  84. package/templates/javascript-live-cursors/vite.config.ts +5 -0
  85. package/templates/live-cursors/.env.example +9 -0
  86. package/templates/live-cursors/README.md +20 -0
  87. package/templates/live-cursors/index.html +12 -0
  88. package/templates/live-cursors/package.json +25 -0
  89. package/templates/live-cursors/src/App.tsx +214 -0
  90. package/templates/live-cursors/src/index.css +94 -0
  91. package/templates/live-cursors/src/main.tsx +10 -0
  92. package/templates/live-cursors/src/vite-env.d.ts +12 -0
  93. package/templates/live-cursors/tsconfig.json +21 -0
  94. package/templates/live-cursors/vite.config.ts +7 -0
  95. package/templates/live-cursors-chat/.env.example +9 -0
  96. package/templates/live-cursors-chat/README.md +11 -0
  97. package/templates/live-cursors-chat/index.html +12 -0
  98. package/templates/live-cursors-chat/package.json +25 -0
  99. package/templates/live-cursors-chat/src/App.tsx +241 -0
  100. package/templates/live-cursors-chat/src/index.css +94 -0
  101. package/templates/live-cursors-chat/src/main.tsx +10 -0
  102. package/templates/live-cursors-chat/src/vite-env.d.ts +12 -0
  103. package/templates/live-cursors-chat/tsconfig.json +21 -0
  104. package/templates/live-cursors-chat/vite.config.ts +7 -0
  105. package/templates/minimal/package.json +4 -4
  106. package/templates/react/package.json +2 -2
  107. package/templates/react/src/App.tsx +8 -2
  108. package/templates/slack/package.json +1 -1
  109. package/templates/telegram/package.json +1 -1
  110. package/templates/tiptap-room/.env.example +9 -0
  111. package/templates/tiptap-room/README.md +14 -0
  112. package/templates/tiptap-room/index.html +12 -0
  113. package/templates/tiptap-room/package.json +29 -0
  114. package/templates/tiptap-room/src/App.tsx +217 -0
  115. package/templates/tiptap-room/src/index.css +107 -0
  116. package/templates/tiptap-room/src/main.tsx +10 -0
  117. package/templates/tiptap-room/src/vite-env.d.ts +12 -0
  118. package/templates/tiptap-room/tsconfig.json +21 -0
  119. package/templates/tiptap-room/vite.config.ts +7 -0
  120. package/templates/voice-stage/.env.example +11 -0
  121. package/templates/voice-stage/README.md +11 -0
  122. package/templates/voice-stage/index.html +12 -0
  123. package/templates/voice-stage/package.json +25 -0
  124. package/templates/voice-stage/src/App.tsx +61 -0
  125. package/templates/voice-stage/src/index.css +75 -0
  126. package/templates/voice-stage/src/main.tsx +10 -0
  127. package/templates/voice-stage/src/session.ts +73 -0
  128. package/templates/voice-stage/src/vite-env.d.ts +13 -0
  129. package/templates/voice-stage/tsconfig.json +21 -0
  130. package/templates/voice-stage/vite.config.ts +7 -0
  131. package/templates/war-room/.env.example +11 -0
  132. package/templates/war-room/README.md +11 -0
  133. package/templates/war-room/index.html +12 -0
  134. package/templates/war-room/package.json +26 -0
  135. package/templates/war-room/src/App.tsx +120 -0
  136. package/templates/war-room/src/index.css +75 -0
  137. package/templates/war-room/src/main.tsx +10 -0
  138. package/templates/war-room/src/session.ts +73 -0
  139. package/templates/war-room/src/vite-env.d.ts +13 -0
  140. package/templates/war-room/tsconfig.json +21 -0
  141. package/templates/war-room/vite.config.ts +7 -0
  142. package/templates/whiteboard/.env.example +11 -0
  143. package/templates/whiteboard/README.md +9 -0
  144. package/templates/whiteboard/index.html +12 -0
  145. package/templates/whiteboard/package.json +26 -0
  146. package/templates/whiteboard/src/App.tsx +122 -0
  147. package/templates/whiteboard/src/index.css +75 -0
  148. package/templates/whiteboard/src/main.tsx +10 -0
  149. package/templates/whiteboard/src/session.ts +73 -0
  150. package/templates/whiteboard/src/vite-env.d.ts +13 -0
  151. package/templates/whiteboard/tsconfig.json +21 -0
  152. package/templates/whiteboard/vite.config.ts +7 -0
@@ -0,0 +1,110 @@
1
+ import { useState } from "react";
2
+ import { FluxyRealtimeProvider, useChat, useFluxyChat } from "@fluxy-chat/react";
3
+ import { useFluxySession, workerUrl } from "./session";
4
+
5
+ function DealBoard({ roomId }: { roomId: string }) {
6
+ const { client } = useFluxyChat();
7
+ const { messages, sendMessage, connected } = useChat({ roomId, replay: "request" });
8
+ const [draft, setDraft] = useState("Approve term sheet?");
9
+ const [busy, setBusy] = useState(false);
10
+
11
+ async function propose() {
12
+ const content = draft.trim();
13
+ if (!client || !content) return;
14
+ setBusy(true);
15
+ try {
16
+ await client.createDecision(roomId, { content, requiredAcks: 2 });
17
+ setDraft("");
18
+ } catch (err) {
19
+ console.error(err);
20
+ await sendMessage(`[decision failed] ${content}`);
21
+ } finally {
22
+ setBusy(false);
23
+ }
24
+ }
25
+
26
+ async function ack(messageId: number) {
27
+ if (!client) return;
28
+ setBusy(true);
29
+ try {
30
+ await client.ackDecision(messageId);
31
+ } finally {
32
+ setBusy(false);
33
+ }
34
+ }
35
+
36
+ async function exportMarkdown() {
37
+ if (!client) return;
38
+ const blob = await client.exportRoomMarkdown(roomId);
39
+ if (!blob) return;
40
+ const url = URL.createObjectURL(blob);
41
+ const link = document.createElement("a");
42
+ link.href = url;
43
+ link.download = `${roomId}.md`;
44
+ link.click();
45
+ URL.revokeObjectURL(url);
46
+ }
47
+
48
+ return (
49
+ <section className="panel">
50
+ <header className="chat-header">
51
+ <strong>Deal room · {roomId}</strong>
52
+ <span className="status">{connected ? "live" : "connecting"} · quorum + export</span>
53
+ </header>
54
+ <p className="hint" style={{ margin: "0.75rem" }}>
55
+ Cross-org invites stay on POST /cross-org (server). This demo is quorum acks on chat messages.
56
+ </p>
57
+ <div className="log">
58
+ {messages.map((msg) => (
59
+ <p key={msg.id}>
60
+ <strong>{msg.userId}:</strong> {msg.content}
61
+ {msg.decision ? (
62
+ <>
63
+ {" "}
64
+ · quorum {msg.decision.totalCurrent}/{msg.decision.totalRequired}
65
+ {msg.decision.quorumMet ? " met" : ""}
66
+ {msg.decision.state === "pending" ? (
67
+ <button type="button" className="primary" style={{ marginLeft: 8 }} disabled={busy} onClick={() => void ack(msg.id)}>
68
+ Ack
69
+ </button>
70
+ ) : null}
71
+ </>
72
+ ) : null}
73
+ </p>
74
+ ))}
75
+ </div>
76
+ <form
77
+ className="composer"
78
+ onSubmit={(event) => {
79
+ event.preventDefault();
80
+ void propose();
81
+ }}
82
+ >
83
+ <input value={draft} onChange={(event) => setDraft(event.target.value)} placeholder="Decision text…" />
84
+ <button type="submit" disabled={busy}>
85
+ Propose
86
+ </button>
87
+ <button type="button" className="primary" onClick={() => void exportMarkdown()}>
88
+ Export .md
89
+ </button>
90
+ </form>
91
+ </section>
92
+ );
93
+ }
94
+
95
+ export function App() {
96
+ const { session, loading, error } = useFluxySession();
97
+ if (!workerUrl) return <div className="shell">Set <code>VITE_FLUXYCHAT_WORKER_URL</code>.</div>;
98
+ if (loading) return <div className="shell">Starting…</div>;
99
+ if (error) return <div className="shell error">{error}</div>;
100
+ if (!session) return <div className="shell">Set a member JWT or public room id.</div>;
101
+
102
+ return (
103
+ <div className="shell">
104
+ <p className="mode-badge">{session.mode} · open two tabs and ack the same decision</p>
105
+ <FluxyRealtimeProvider workerUrl={session.workerUrl} authTokenProvider={session.token} userId={session.userId}>
106
+ <DealBoard roomId={session.roomId} />
107
+ </FluxyRealtimeProvider>
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,75 @@
1
+ :root {
2
+ font-family: system-ui, -apple-system, sans-serif;
3
+ line-height: 1.5;
4
+ color: #0f172a;
5
+ background: #f8fafc;
6
+ }
7
+ * { box-sizing: border-box; }
8
+ body { margin: 0; }
9
+ .shell { max-width: 720px; margin: 0 auto; padding: 1.5rem; }
10
+ .panel {
11
+ border: 1px solid #e2e8f0;
12
+ border-radius: 12px;
13
+ overflow: hidden;
14
+ background: #fff;
15
+ }
16
+ .chat-header {
17
+ display: flex;
18
+ justify-content: space-between;
19
+ padding: 0.75rem 1rem;
20
+ background: #0f172a;
21
+ color: #fff;
22
+ }
23
+ .status { font-size: 0.75rem; opacity: 0.9; }
24
+ .roster {
25
+ margin: 0;
26
+ padding: 0.5rem 1rem;
27
+ list-style: none;
28
+ font-size: 0.8rem;
29
+ color: #64748b;
30
+ border-bottom: 1px solid #e2e8f0;
31
+ }
32
+ .log { min-height: 240px; max-height: 50vh; overflow: auto; padding: 1rem; }
33
+ .composer { display: flex; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid #e2e8f0; }
34
+ .composer input { flex: 1; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 8px; }
35
+ .composer button, button.primary {
36
+ padding: 0.5rem 0.75rem;
37
+ border: 0;
38
+ border-radius: 8px;
39
+ background: #2563eb;
40
+ color: #fff;
41
+ cursor: pointer;
42
+ }
43
+ .mode-badge { font-size: 0.75rem; color: #64748b; margin-bottom: 0.75rem; }
44
+ .error { color: #b91c1c; }
45
+ code { font-size: 0.85em; }
46
+ .hint { font-size: 0.8rem; color: #475569; white-space: pre-wrap; background: #f1f5f9; padding: 0.75rem; border-radius: 8px; }
47
+ .readings { margin: 0; padding: 0.75rem 1rem; list-style: none; font-size: 0.875rem; }
48
+ .canvas {
49
+ position: relative;
50
+ height: min(70vh, 480px);
51
+ background: #f8fafc;
52
+ touch-action: none;
53
+ }
54
+ .dot {
55
+ position: absolute;
56
+ width: 10px;
57
+ height: 10px;
58
+ border-radius: 999px;
59
+ transform: translate(-50%, -50%);
60
+ pointer-events: none;
61
+ }
62
+ .peer-cursor {
63
+ position: absolute;
64
+ pointer-events: none;
65
+ z-index: 2;
66
+ }
67
+ .peer-cursor span {
68
+ display: block;
69
+ margin-top: 2px;
70
+ padding: 1px 6px;
71
+ border-radius: 4px;
72
+ background: currentColor;
73
+ color: #fff;
74
+ font-size: 11px;
75
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./index.css";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -0,0 +1,73 @@
1
+ import { useEffect, useState } from "react";
2
+ import { FluxyChatClient } from "@fluxy-chat/sdk";
3
+
4
+ const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
5
+ const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
6
+ const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
7
+ const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
8
+
9
+ export interface FluxySession {
10
+ workerUrl: string;
11
+ token: string;
12
+ userId: string;
13
+ roomId: string;
14
+ mode: "member" | "guest";
15
+ }
16
+
17
+ export function useFluxySession(): {
18
+ session: FluxySession | null;
19
+ loading: boolean;
20
+ error: string | null;
21
+ } {
22
+ const [session, setSession] = useState<FluxySession | null>(null);
23
+ const [loading, setLoading] = useState(Boolean(workerUrl));
24
+ const [error, setError] = useState<string | null>(null);
25
+
26
+ useEffect(() => {
27
+ if (!workerUrl) {
28
+ setLoading(false);
29
+ return;
30
+ }
31
+ if (memberJwt) {
32
+ setSession({
33
+ workerUrl,
34
+ token: memberJwt,
35
+ userId: "demo-user",
36
+ roomId: configuredRoomId,
37
+ mode: "member",
38
+ });
39
+ setLoading(false);
40
+ return;
41
+ }
42
+ if (publicRoomId) {
43
+ let cancelled = false;
44
+ void FluxyChatClient.joinPublicRoomAsGuest(workerUrl, publicRoomId, {
45
+ displayName: "Guest",
46
+ })
47
+ .then((guest) => {
48
+ if (cancelled) return;
49
+ setSession({
50
+ workerUrl,
51
+ token: guest.token,
52
+ userId: guest.userId,
53
+ roomId: guest.roomId,
54
+ mode: "guest",
55
+ });
56
+ })
57
+ .catch((err) => {
58
+ if (!cancelled) setError(err instanceof Error ? err.message : "Guest session failed");
59
+ })
60
+ .finally(() => {
61
+ if (!cancelled) setLoading(false);
62
+ });
63
+ return () => {
64
+ cancelled = true;
65
+ };
66
+ }
67
+ setLoading(false);
68
+ }, []);
69
+
70
+ return { session, loading, error };
71
+ }
72
+
73
+ export { workerUrl };
@@ -0,0 +1,13 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_FLUXYCHAT_WORKER_URL: string;
5
+ readonly VITE_FLUXYCHAT_MEMBER_JWT?: string;
6
+ readonly VITE_FLUXYCHAT_ROOM_ID?: string;
7
+ readonly VITE_FLUXYCHAT_PUBLIC_ROOM_ID?: string;
8
+ readonly VITE_FLUXYCHAT_AGENT_ID?: string;
9
+ }
10
+
11
+ interface ImportMeta {
12
+ readonly env: ImportMetaEnv;
13
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "isolatedModules": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noUncheckedSideEffectImports": true
19
+ },
20
+ "include": ["src"]
21
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: { port: 5173 },
7
+ });
@@ -9,7 +9,7 @@
9
9
  "type-check": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
- "@fluxy-chat/sdk": "^0.6.0",
12
+ "@fluxy-chat/sdk": "^0.6.4",
13
13
  "discord.js": "^14.0.0"
14
14
  },
15
15
  "devDependencies": {
@@ -0,0 +1,7 @@
1
+ # FluxyChat worker URL
2
+ VITE_FLUXYCHAT_WORKER_URL=https://your-worker.example.workers.dev
3
+
4
+ VITE_FLUXYCHAT_MEMBER_JWT=eyJ...
5
+ VITE_FLUXYCHAT_ROOM_ID=demo
6
+
7
+ # VITE_FLUXYCHAT_PUBLIC_ROOM_ID=your-public-room-id
@@ -0,0 +1,7 @@
1
+ # FluxyChat draw
2
+
3
+ Live cursors (`type: "cursor"`) plus sparse stamps (`client-ephemeral-dot`). Open **two tabs**. Not a second CRDT.
4
+
5
+ ```bash
6
+ npx @fluxy-chat/create-fluxy-chat@latest my-draw --example draw
7
+ ```
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>FluxyChat draw</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "fluxy-draw",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@fluxy-chat/react": "^0.1.4",
13
+ "@fluxy-chat/sdk": "^0.6.4",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0",
16
+ "zustand": "^5.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^19.0.0",
20
+ "@types/react-dom": "^19.0.0",
21
+ "@vitejs/plugin-react": "^4.3.0",
22
+ "typescript": "^5.6.0",
23
+ "vite": "^6.0.0"
24
+ }
25
+ }
@@ -0,0 +1,91 @@
1
+ import { useEffect, useState } from "react";
2
+ import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/react";
3
+ import { useFluxySession, workerUrl } from "./session";
4
+
5
+ interface Dot {
6
+ x: number;
7
+ y: number;
8
+ color: string;
9
+ }
10
+
11
+ function DrawBoard({ roomId, selfUserId }: { roomId: string; selfUserId: string }) {
12
+ const { liveCursors, sendCursor, sendClientEvent, lastClientEvent, connected } = useChat({
13
+ roomId,
14
+ replay: "request",
15
+ });
16
+ const [dots, setDots] = useState<Dot[]>([]);
17
+
18
+ useEffect(() => {
19
+ if (!lastClientEvent || lastClientEvent.eventName !== "client-ephemeral-dot") return;
20
+ if (lastClientEvent.userId === selfUserId) return;
21
+ const data = lastClientEvent.data;
22
+ if (!data || typeof data !== "object") return;
23
+ const rec = data as { x?: number; y?: number; color?: string };
24
+ const x = Number(rec.x);
25
+ const y = Number(rec.y);
26
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return;
27
+ setDots((prev) => [...prev, { x, y, color: String(rec.color ?? "#2563eb") }]);
28
+ }, [lastClientEvent, selfUserId]);
29
+
30
+ return (
31
+ <section className="panel">
32
+ <header className="chat-header">
33
+ <strong>Draw · {roomId}</strong>
34
+ <span className="status">{connected ? "live" : "connecting"} — click to stamp, move for cursors</span>
35
+ </header>
36
+ <div
37
+ className="canvas"
38
+ onPointerMove={(event) => {
39
+ const rect = event.currentTarget.getBoundingClientRect();
40
+ sendCursor({
41
+ x: event.clientX - rect.left,
42
+ y: event.clientY - rect.top,
43
+ color: "#db2777",
44
+ label: selfUserId.slice(0, 12),
45
+ });
46
+ }}
47
+ onPointerDown={(event) => {
48
+ const rect = event.currentTarget.getBoundingClientRect();
49
+ const x = event.clientX - rect.left;
50
+ const y = event.clientY - rect.top;
51
+ sendClientEvent("client-ephemeral-dot", { x, y, color: "#2563eb" });
52
+ setDots((prev) => [...prev, { x, y, color: "#2563eb" }]);
53
+ }}
54
+ >
55
+ {dots.map((dot, i) => (
56
+ <span key={i} className="dot" style={{ left: dot.x, top: dot.y, background: dot.color }} />
57
+ ))}
58
+ {Object.values(liveCursors)
59
+ .filter((c) => c.userId !== selfUserId)
60
+ .map((cursor) => (
61
+ <div
62
+ key={cursor.userId}
63
+ className="peer-cursor"
64
+ style={{ left: cursor.x, top: cursor.y, color: cursor.color || "#0f172a" }}
65
+ >
66
+ <span>{cursor.label || cursor.userId.slice(0, 8)}</span>
67
+ </div>
68
+ ))}
69
+ </div>
70
+ </section>
71
+ );
72
+ }
73
+
74
+ export function App() {
75
+ const { session, loading, error } = useFluxySession();
76
+ if (!workerUrl) return <div className="shell">Set <code>VITE_FLUXYCHAT_WORKER_URL</code>.</div>;
77
+ if (loading) return <div className="shell">Starting…</div>;
78
+ if (error) return <div className="shell error">{error}</div>;
79
+ if (!session) return <div className="shell">Set a member JWT or public room id.</div>;
80
+
81
+ return (
82
+ <div className="shell">
83
+ <p className="mode-badge">
84
+ {session.mode} · cursors are type &quot;cursor&quot;; stamps are client-ephemeral-dot. Open two tabs.
85
+ </p>
86
+ <FluxyRealtimeProvider workerUrl={session.workerUrl} authTokenProvider={session.token} userId={session.userId}>
87
+ <DrawBoard roomId={session.roomId} selfUserId={session.userId} />
88
+ </FluxyRealtimeProvider>
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,75 @@
1
+ :root {
2
+ font-family: system-ui, -apple-system, sans-serif;
3
+ line-height: 1.5;
4
+ color: #0f172a;
5
+ background: #f8fafc;
6
+ }
7
+ * { box-sizing: border-box; }
8
+ body { margin: 0; }
9
+ .shell { max-width: 720px; margin: 0 auto; padding: 1.5rem; }
10
+ .panel {
11
+ border: 1px solid #e2e8f0;
12
+ border-radius: 12px;
13
+ overflow: hidden;
14
+ background: #fff;
15
+ }
16
+ .chat-header {
17
+ display: flex;
18
+ justify-content: space-between;
19
+ padding: 0.75rem 1rem;
20
+ background: #0f172a;
21
+ color: #fff;
22
+ }
23
+ .status { font-size: 0.75rem; opacity: 0.9; }
24
+ .roster {
25
+ margin: 0;
26
+ padding: 0.5rem 1rem;
27
+ list-style: none;
28
+ font-size: 0.8rem;
29
+ color: #64748b;
30
+ border-bottom: 1px solid #e2e8f0;
31
+ }
32
+ .log { min-height: 240px; max-height: 50vh; overflow: auto; padding: 1rem; }
33
+ .composer { display: flex; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid #e2e8f0; }
34
+ .composer input { flex: 1; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 8px; }
35
+ .composer button, button.primary {
36
+ padding: 0.5rem 0.75rem;
37
+ border: 0;
38
+ border-radius: 8px;
39
+ background: #2563eb;
40
+ color: #fff;
41
+ cursor: pointer;
42
+ }
43
+ .mode-badge { font-size: 0.75rem; color: #64748b; margin-bottom: 0.75rem; }
44
+ .error { color: #b91c1c; }
45
+ code { font-size: 0.85em; }
46
+ .hint { font-size: 0.8rem; color: #475569; white-space: pre-wrap; background: #f1f5f9; padding: 0.75rem; border-radius: 8px; }
47
+ .readings { margin: 0; padding: 0.75rem 1rem; list-style: none; font-size: 0.875rem; }
48
+ .canvas {
49
+ position: relative;
50
+ height: min(70vh, 480px);
51
+ background: #f8fafc;
52
+ touch-action: none;
53
+ }
54
+ .dot {
55
+ position: absolute;
56
+ width: 10px;
57
+ height: 10px;
58
+ border-radius: 999px;
59
+ transform: translate(-50%, -50%);
60
+ pointer-events: none;
61
+ }
62
+ .peer-cursor {
63
+ position: absolute;
64
+ pointer-events: none;
65
+ z-index: 2;
66
+ }
67
+ .peer-cursor span {
68
+ display: block;
69
+ margin-top: 2px;
70
+ padding: 1px 6px;
71
+ border-radius: 4px;
72
+ background: currentColor;
73
+ color: #fff;
74
+ font-size: 11px;
75
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./index.css";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -0,0 +1,73 @@
1
+ import { useEffect, useState } from "react";
2
+ import { FluxyChatClient } from "@fluxy-chat/sdk";
3
+
4
+ const workerUrl = import.meta.env.VITE_FLUXYCHAT_WORKER_URL?.trim();
5
+ const memberJwt = import.meta.env.VITE_FLUXYCHAT_MEMBER_JWT?.trim();
6
+ const publicRoomId = import.meta.env.VITE_FLUXYCHAT_PUBLIC_ROOM_ID?.trim();
7
+ const configuredRoomId = import.meta.env.VITE_FLUXYCHAT_ROOM_ID?.trim() || "demo";
8
+
9
+ export interface FluxySession {
10
+ workerUrl: string;
11
+ token: string;
12
+ userId: string;
13
+ roomId: string;
14
+ mode: "member" | "guest";
15
+ }
16
+
17
+ export function useFluxySession(): {
18
+ session: FluxySession | null;
19
+ loading: boolean;
20
+ error: string | null;
21
+ } {
22
+ const [session, setSession] = useState<FluxySession | null>(null);
23
+ const [loading, setLoading] = useState(Boolean(workerUrl));
24
+ const [error, setError] = useState<string | null>(null);
25
+
26
+ useEffect(() => {
27
+ if (!workerUrl) {
28
+ setLoading(false);
29
+ return;
30
+ }
31
+ if (memberJwt) {
32
+ setSession({
33
+ workerUrl,
34
+ token: memberJwt,
35
+ userId: "demo-user",
36
+ roomId: configuredRoomId,
37
+ mode: "member",
38
+ });
39
+ setLoading(false);
40
+ return;
41
+ }
42
+ if (publicRoomId) {
43
+ let cancelled = false;
44
+ void FluxyChatClient.joinPublicRoomAsGuest(workerUrl, publicRoomId, {
45
+ displayName: "Guest",
46
+ })
47
+ .then((guest) => {
48
+ if (cancelled) return;
49
+ setSession({
50
+ workerUrl,
51
+ token: guest.token,
52
+ userId: guest.userId,
53
+ roomId: guest.roomId,
54
+ mode: "guest",
55
+ });
56
+ })
57
+ .catch((err) => {
58
+ if (!cancelled) setError(err instanceof Error ? err.message : "Guest session failed");
59
+ })
60
+ .finally(() => {
61
+ if (!cancelled) setLoading(false);
62
+ });
63
+ return () => {
64
+ cancelled = true;
65
+ };
66
+ }
67
+ setLoading(false);
68
+ }, []);
69
+
70
+ return { session, loading, error };
71
+ }
72
+
73
+ export { workerUrl };
@@ -0,0 +1,13 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_FLUXYCHAT_WORKER_URL: string;
5
+ readonly VITE_FLUXYCHAT_MEMBER_JWT?: string;
6
+ readonly VITE_FLUXYCHAT_ROOM_ID?: string;
7
+ readonly VITE_FLUXYCHAT_PUBLIC_ROOM_ID?: string;
8
+ readonly VITE_FLUXYCHAT_AGENT_ID?: string;
9
+ }
10
+
11
+ interface ImportMeta {
12
+ readonly env: ImportMetaEnv;
13
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "isolatedModules": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noUncheckedSideEffectImports": true
19
+ },
20
+ "include": ["src"]
21
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: { port: 5173 },
7
+ });