@nuraly/lumenjs 0.2.0 → 0.5.0

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 (35) hide show
  1. package/README.md +80 -7
  2. package/dist/build/build-markdown.d.ts +15 -0
  3. package/dist/build/build-markdown.js +90 -0
  4. package/dist/build/build-server.js +13 -2
  5. package/dist/build/build.js +34 -2
  6. package/dist/build/scan.js +4 -1
  7. package/dist/build/serve-loaders.js +12 -2
  8. package/dist/build/serve-ssr.js +12 -3
  9. package/dist/build/serve-static.js +2 -1
  10. package/dist/build/serve.js +1 -1
  11. package/dist/communication/server.js +1 -0
  12. package/dist/communication/signaling.d.ts +2 -0
  13. package/dist/communication/signaling.js +41 -0
  14. package/dist/dev-server/plugins/vite-plugin-llms.js +1 -0
  15. package/dist/dev-server/plugins/vite-plugin-loaders.d.ts +4 -3
  16. package/dist/dev-server/plugins/vite-plugin-loaders.js +20 -5
  17. package/dist/dev-server/ssr-render.js +15 -3
  18. package/dist/editor/ai/types.d.ts +1 -1
  19. package/dist/editor/ai/types.js +2 -2
  20. package/dist/llms/generate.d.ts +15 -1
  21. package/dist/llms/generate.js +54 -44
  22. package/dist/runtime/communication.d.ts +65 -36
  23. package/dist/runtime/communication.js +117 -57
  24. package/dist/runtime/router.js +3 -3
  25. package/dist/runtime/webrtc.d.ts +8 -1
  26. package/dist/runtime/webrtc.js +49 -15
  27. package/dist/shared/html-to-markdown.d.ts +6 -0
  28. package/dist/shared/html-to-markdown.js +73 -0
  29. package/dist/shared/utils.js +8 -0
  30. package/package.json +2 -2
  31. package/templates/blog/pages/index.ts +3 -3
  32. package/templates/blog/pages/posts/[slug].ts +17 -6
  33. package/templates/blog/pages/tag/[tag].ts +6 -6
  34. package/templates/dashboard/pages/index.ts +7 -7
  35. package/templates/default/pages/index.ts +3 -3
@@ -14,6 +14,8 @@ export interface LlmsApiRoute {
14
14
  }
15
15
  export interface LlmsTxtInput {
16
16
  title: string;
17
+ description?: string;
18
+ baseUrl?: string;
17
19
  pages: LlmsPage[];
18
20
  apiRoutes: LlmsApiRoute[];
19
21
  integrations: string[];
@@ -26,9 +28,21 @@ export interface LlmsTxtInput {
26
28
  };
27
29
  }
28
30
  /**
29
- * Generate the llms.txt content from project metadata and resolved page data.
31
+ * Generate the llms.txt content following the llmstxt.org spec.
32
+ *
33
+ * Structure: H1 title, blockquote summary, H2 sections with
34
+ * markdown links to each page (linking to .md versions).
30
35
  */
31
36
  export declare function generateLlmsTxt(input: LlmsTxtInput): string;
37
+ /**
38
+ * Generate llms-full.txt — all page content inlined as one markdown document.
39
+ */
40
+ export declare function generateLlmsFullTxt(input: LlmsTxtInput & {
41
+ pageContents: {
42
+ path: string;
43
+ markdown: string;
44
+ }[];
45
+ }): string;
32
46
  /**
33
47
  * Try to resolve dynamic route entries by finding a parent/sibling index page
34
48
  * whose loader returns an array, then calling the dynamic page's loader for each item.
@@ -1,62 +1,53 @@
1
1
  /**
2
- * Generate the llms.txt content from project metadata and resolved page data.
2
+ * Generate the llms.txt content following the llmstxt.org spec.
3
+ *
4
+ * Structure: H1 title, blockquote summary, H2 sections with
5
+ * markdown links to each page (linking to .md versions).
3
6
  */
4
7
  export function generateLlmsTxt(input) {
5
8
  const lines = [];
9
+ const base = input.baseUrl ? input.baseUrl.replace(/\/$/, '') : '';
6
10
  lines.push(`# ${input.title}`);
7
11
  lines.push('');
8
- lines.push('> Built with LumenJS');
12
+ lines.push(`> ${input.description || `${input.title}. Built with LumenJS.`}`);
9
13
  lines.push('');
10
- // Pages section
11
- if (input.pages.length > 0) {
12
- lines.push('## Pages');
14
+ // Group pages by top-level path segment
15
+ const groups = new Map();
16
+ for (const page of input.pages) {
17
+ const segments = page.path.split('/').filter(Boolean);
18
+ const section = segments.length > 0 ? segments[0] : 'pages';
19
+ if (!groups.has(section))
20
+ groups.set(section, []);
21
+ groups.get(section).push(page);
22
+ }
23
+ // Pages — grouped by section with links
24
+ for (const [section, pages] of groups) {
25
+ const sectionTitle = section.charAt(0).toUpperCase() + section.slice(1);
26
+ lines.push(`## ${sectionTitle}`);
13
27
  lines.push('');
14
- for (const page of input.pages) {
15
- lines.push(`### ${page.path}`);
28
+ for (const page of pages) {
16
29
  const isDynamic = page.path.includes(':');
17
- if (isDynamic) {
18
- // Dynamic route — show expanded entries if available
19
- if (page.dynamicEntries && page.dynamicEntries.length > 0) {
20
- lines.push(`Dynamic route — ${page.dynamicEntries.length} ${page.dynamicEntries.length === 1 ? 'entry' : 'entries'}:`);
21
- lines.push('');
22
- for (const entry of page.dynamicEntries) {
23
- lines.push(`#### ${entry.path}`);
24
- if (entry.loaderData) {
25
- lines.push(flattenData(entry.loaderData));
26
- }
27
- lines.push('');
28
- }
29
- }
30
- else {
31
- lines.push('- Dynamic route');
32
- lines.push('');
33
- }
34
- }
35
- else if (page.loaderData && typeof page.loaderData === 'object') {
36
- // Static page with loader data
37
- lines.push(flattenData(page.loaderData));
38
- lines.push('');
39
- }
40
- else {
41
- // Simple page
42
- const features = [];
43
- if (page.hasLoader)
44
- features.push('with loader data');
45
- if (page.hasSubscribe)
46
- features.push('with live data');
47
- lines.push(`- Server-rendered page${features.length ? ' ' + features.join(', ') : ''}`);
48
- lines.push('');
49
- }
30
+ const label = page.path === '/' ? 'Home' : page.path;
31
+ const features = [];
32
+ if (isDynamic)
33
+ features.push('dynamic');
34
+ if (page.hasLoader)
35
+ features.push('server data');
36
+ if (page.hasSubscribe)
37
+ features.push('live data');
38
+ const desc = features.length > 0 ? `: ${features.join(', ')}` : '';
39
+ // Link to .md version for LLM-readable content (skip dynamic routes)
40
+ const href = !isDynamic ? `${base}${page.path}.md` : `${base}${page.path}`;
41
+ lines.push(`- [${label}](${href})${desc}`);
50
42
  }
43
+ lines.push('');
51
44
  }
52
45
  // API Routes section
53
46
  if (input.apiRoutes.length > 0) {
54
- lines.push('## API Routes');
47
+ lines.push('## API');
55
48
  lines.push('');
56
49
  for (const route of input.apiRoutes) {
57
- for (const method of route.methods) {
58
- lines.push(`- ${method} /api/${route.path}`);
59
- }
50
+ lines.push(`- [${route.methods.join(', ')} /api/${route.path}](${base}/api/${route.path})`);
60
51
  }
61
52
  lines.push('');
62
53
  }
@@ -85,6 +76,25 @@ export function generateLlmsTxt(input) {
85
76
  }
86
77
  return lines.join('\n').trimEnd() + '\n';
87
78
  }
79
+ /**
80
+ * Generate llms-full.txt — all page content inlined as one markdown document.
81
+ */
82
+ export function generateLlmsFullTxt(input) {
83
+ const lines = [];
84
+ lines.push(`# ${input.title}`);
85
+ lines.push('');
86
+ lines.push(`> ${input.description || `${input.title}. Built with LumenJS.`}`);
87
+ lines.push('');
88
+ for (const page of input.pageContents) {
89
+ lines.push('---');
90
+ lines.push(`source: ${page.path}`);
91
+ lines.push('---');
92
+ lines.push('');
93
+ lines.push(page.markdown.trim());
94
+ lines.push('');
95
+ }
96
+ return lines.join('\n').trimEnd() + '\n';
97
+ }
88
98
  /**
89
99
  * Flatten a loader data object into key-value text lines.
90
100
  */
@@ -1,26 +1,65 @@
1
1
  /**
2
2
  * Client-side communication SDK.
3
- * Connects to the server via Socket.io and provides a clean API for chat, typing, and presence.
3
+ * Connects to the server via Socket.io and provides a clean API for chat, typing, presence, and calls.
4
4
  */
5
5
  /**
6
6
  * Connect to the communication socket.
7
- * Must be called before using any other functions.
7
+ * Reuses an existing socket if one is already connected.
8
8
  */
9
- export declare function connectChat(params?: Record<string, string>): Promise<void>;
10
- /** Join a conversation room to receive messages */
11
- export declare function joinConversation(conversationId: string): void;
12
- /** Leave a conversation room */
13
- export declare function leaveConversation(conversationId: string): void;
14
- /** Send a message */
15
- export declare function sendMessage(conversationId: string, content: string, type?: string): void;
16
- /** Mark a message as read */
17
- export declare function markRead(conversationId: string, messageId: string): void;
9
+ export declare function connectChat(params?: Record<string, string>): Promise<any>;
10
+ /** Get the underlying socket instance (for call-service or other integrations) */
11
+ export declare function getSocket(): any;
12
+ /** Set an externally-created socket (e.g. from call-service or router) */
13
+ export declare function setSocket(socket: any): void;
14
+ /** Send a message (text, image, file, audio) */
15
+ export declare function sendMessage(conversationId: string, content: string, opts?: {
16
+ type?: string;
17
+ attachment?: any;
18
+ replyTo?: any;
19
+ encrypted?: boolean;
20
+ }): void;
21
+ /** Mark messages as read */
22
+ export declare function markRead(conversationId: string, messageIds?: string[]): void;
18
23
  /** React to a message with an emoji (toggle) */
19
24
  export declare function reactToMessage(messageId: string, conversationId: string, emoji: string): void;
20
25
  /** Edit a message */
21
26
  export declare function editMessage(messageId: string, conversationId: string, content: string): void;
22
27
  /** Delete a message */
23
28
  export declare function deleteMessage(messageId: string, conversationId: string): void;
29
+ /** Load messages for a conversation (lazy-load) */
30
+ export declare function loadMessages(conversationId: string): void;
31
+ /** Join a conversation room to receive messages */
32
+ export declare function joinConversation(conversationId: string): void;
33
+ /** Leave a conversation room */
34
+ export declare function leaveConversation(conversationId: string): void;
35
+ /** Start typing indicator */
36
+ export declare function startTyping(conversationId: string): void;
37
+ /** Stop typing indicator */
38
+ export declare function stopTyping(conversationId: string): void;
39
+ /** Update presence status */
40
+ export declare function updatePresence(status: 'online' | 'offline' | 'away' | 'busy'): void;
41
+ /** Request bulk presence sync for a list of user IDs */
42
+ export declare function requestPresenceSync(userIds: string[]): void;
43
+ /** Refresh notification/message badge counts */
44
+ export declare function refreshBadge(): void;
45
+ /** Listen for new messages */
46
+ export declare function onMessage(handler: (message: any) => void): () => void;
47
+ /** Listen for typing updates */
48
+ export declare function onTyping(handler: (data: {
49
+ conversationId: string;
50
+ userId: string;
51
+ isTyping: boolean;
52
+ }) => void): () => void;
53
+ /** Listen for presence changes */
54
+ export declare function onPresence(handler: (data: {
55
+ userId: string;
56
+ status: string;
57
+ lastSeen: string;
58
+ }) => void): () => void;
59
+ /** Listen for bulk presence sync response */
60
+ export declare function onPresenceSync(handler: (data: {
61
+ presences: Record<string, any>;
62
+ }) => void): () => void;
24
63
  /** Listen for reaction updates */
25
64
  export declare function onReactionUpdate(handler: (data: {
26
65
  messageId: string;
@@ -37,6 +76,14 @@ export declare function onMessageDeleted(handler: (data: {
37
76
  messageId: string;
38
77
  conversationId: string;
39
78
  }) => void): () => void;
79
+ /** Listen for read receipts */
80
+ export declare function onReadReceipt(handler: (data: any) => void): () => void;
81
+ /** Listen for lazy-loaded conversation messages */
82
+ export declare function onConversationMessages(handler: (data: {
83
+ conversationId: string;
84
+ messages: any[];
85
+ participants: any[];
86
+ }) => void): () => void;
40
87
  /** Upload a file (returns attachment metadata) */
41
88
  export declare function uploadFile(file: Blob, filename: string, encrypted?: boolean): Promise<{
42
89
  id: string;
@@ -45,31 +92,9 @@ export declare function uploadFile(file: Blob, filename: string, encrypted?: boo
45
92
  }>;
46
93
  /** Fetch link previews for a text */
47
94
  export declare function fetchLinkPreviews(text: string): Promise<any[]>;
48
- /** Start typing indicator */
49
- export declare function startTyping(conversationId: string): void;
50
- /** Stop typing indicator */
51
- export declare function stopTyping(conversationId: string): void;
52
- /** Update presence status */
53
- export declare function updatePresence(status: 'online' | 'offline' | 'away' | 'busy'): void;
54
- /** Listen for new messages */
55
- export declare function onMessage(handler: (message: any) => void): () => void;
56
- /** Listen for typing updates */
57
- export declare function onTyping(handler: (data: {
58
- conversationId: string;
59
- userId: string;
60
- isTyping: boolean;
61
- }) => void): () => void;
62
- /** Listen for presence changes */
63
- export declare function onPresence(handler: (data: {
64
- userId: string;
65
- status: string;
66
- lastSeen: string;
67
- }) => void): () => void;
68
- /** Listen for read receipts */
69
- export declare function onReadReceipt(handler: (data: any) => void): () => void;
70
95
  /** Listen for incoming calls */
71
96
  export declare function onIncomingCall(handler: (call: any) => void): () => void;
72
- /** Listen for call state changes (initiating, ringing, connecting, connected, ended) */
97
+ /** Listen for call state changes */
73
98
  export declare function onCallStateChanged(handler: (data: {
74
99
  callId: string;
75
100
  state: string;
@@ -94,11 +119,15 @@ export declare function onMediaChanged(handler: (data: {
94
119
  screenShare?: boolean;
95
120
  }) => void): () => void;
96
121
  /** Initiate an audio or video call */
97
- export declare function initiateCall(conversationId: string, type: 'audio' | 'video', calleeIds: string[]): void;
122
+ export declare function initiateCall(conversationId: string, type: 'audio' | 'video', calleeIds: string[], caller?: {
123
+ callerName?: string;
124
+ callerInitials?: string;
125
+ callerColor?: string;
126
+ }): void;
98
127
  /** Respond to an incoming call */
99
128
  export declare function respondToCall(callId: string, action: 'accept' | 'reject'): void;
100
129
  /** Hang up an active call */
101
- export declare function hangup(callId: string, reason?: string): void;
130
+ export declare function hangup(callId: string, reason?: string, duration?: string | null): void;
102
131
  /** Toggle audio/video/screenshare during a call */
103
132
  export declare function toggleMedia(callId: string, opts: {
104
133
  audio?: boolean;
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Client-side communication SDK.
3
- * Connects to the server via Socket.io and provides a clean API for chat, typing, and presence.
3
+ * Connects to the server via Socket.io and provides a clean API for chat, typing, presence, and calls.
4
4
  */
5
5
  let _socket = null;
6
6
  let _handlers = new Map();
7
7
  function emit(event, data) {
8
8
  if (!_socket)
9
- throw new Error('Communication not connected. Call connectChat() first.');
9
+ return;
10
10
  _socket.emit(`nk:${event}`, data);
11
11
  }
12
12
  function addHandler(event, handler) {
@@ -22,15 +22,19 @@ function removeHandler(event, handler) {
22
22
  }
23
23
  /**
24
24
  * Connect to the communication socket.
25
- * Must be called before using any other functions.
25
+ * Reuses an existing socket if one is already connected.
26
26
  */
27
27
  export async function connectChat(params) {
28
- if (_socket)
29
- return;
28
+ if (_socket?.connected)
29
+ return _socket;
30
30
  const { io } = await import('socket.io-client');
31
31
  _socket = io('/nk/messages', {
32
32
  path: '/__nk_socketio/',
33
33
  query: { ...params, __params: JSON.stringify(params || {}) },
34
+ reconnection: true,
35
+ reconnectionAttempts: Infinity,
36
+ reconnectionDelay: 1000,
37
+ reconnectionDelayMax: 10000,
34
38
  });
35
39
  _socket.on('nk:data', (data) => {
36
40
  if (data?.event) {
@@ -41,22 +45,44 @@ export async function connectChat(params) {
41
45
  }
42
46
  }
43
47
  });
48
+ return _socket;
44
49
  }
45
- /** Join a conversation room to receive messages */
46
- export function joinConversation(conversationId) {
47
- emit('conversation:join', { conversationId });
48
- }
49
- /** Leave a conversation room */
50
- export function leaveConversation(conversationId) {
51
- emit('conversation:leave', { conversationId });
50
+ /** Get the underlying socket instance (for call-service or other integrations) */
51
+ export function getSocket() { return _socket; }
52
+ /** Set an externally-created socket (e.g. from call-service or router) */
53
+ export function setSocket(socket) {
54
+ if (_socket === socket)
55
+ return;
56
+ _socket = socket;
57
+ // Attach the nk:data handler if not already attached
58
+ if (_socket && !_socket.__nk_comm_attached) {
59
+ _socket.__nk_comm_attached = true;
60
+ _socket.on('nk:data', (data) => {
61
+ if (data?.event) {
62
+ const handlers = _handlers.get(data.event);
63
+ if (handlers) {
64
+ for (const h of handlers)
65
+ h(data.data);
66
+ }
67
+ }
68
+ });
69
+ }
52
70
  }
53
- /** Send a message */
54
- export function sendMessage(conversationId, content, type = 'text') {
55
- emit('message:send', { conversationId, content, type });
71
+ // ── Messages ─────────────────────────────────────────────────────
72
+ /** Send a message (text, image, file, audio) */
73
+ export function sendMessage(conversationId, content, opts) {
74
+ emit('message:send', {
75
+ conversationId,
76
+ content,
77
+ type: opts?.type || 'text',
78
+ ...(opts?.attachment ? { attachment: opts.attachment } : {}),
79
+ ...(opts?.replyTo ? { replyTo: opts.replyTo } : {}),
80
+ ...(opts?.encrypted ? { encrypted: true } : {}),
81
+ });
56
82
  }
57
- /** Mark a message as read */
58
- export function markRead(conversationId, messageId) {
59
- emit('message:read', { conversationId, messageId });
83
+ /** Mark messages as read */
84
+ export function markRead(conversationId, messageIds) {
85
+ emit('message:read', { conversationId, ...(messageIds ? { messageIds } : {}) });
60
86
  }
61
87
  /** React to a message with an emoji (toggle) */
62
88
  export function reactToMessage(messageId, conversationId, emoji) {
@@ -70,6 +96,61 @@ export function editMessage(messageId, conversationId, content) {
70
96
  export function deleteMessage(messageId, conversationId) {
71
97
  emit('message:delete', { messageId, conversationId });
72
98
  }
99
+ /** Load messages for a conversation (lazy-load) */
100
+ export function loadMessages(conversationId) {
101
+ emit('conversation:load-messages', { conversationId });
102
+ }
103
+ /** Join a conversation room to receive messages */
104
+ export function joinConversation(conversationId) {
105
+ emit('conversation:join', { conversationId });
106
+ }
107
+ /** Leave a conversation room */
108
+ export function leaveConversation(conversationId) {
109
+ emit('conversation:leave', { conversationId });
110
+ }
111
+ // ── Typing ───────────────────────────────────────────────────────
112
+ /** Start typing indicator */
113
+ export function startTyping(conversationId) {
114
+ emit('typing:start', { conversationId });
115
+ }
116
+ /** Stop typing indicator */
117
+ export function stopTyping(conversationId) {
118
+ emit('typing:stop', { conversationId });
119
+ }
120
+ // ── Presence ─────────────────────────────────────────────────────
121
+ /** Update presence status */
122
+ export function updatePresence(status) {
123
+ emit('presence:update', { status });
124
+ }
125
+ /** Request bulk presence sync for a list of user IDs */
126
+ export function requestPresenceSync(userIds) {
127
+ emit('presence:sync', { userIds });
128
+ }
129
+ /** Refresh notification/message badge counts */
130
+ export function refreshBadge() {
131
+ emit('badge:refresh', {});
132
+ }
133
+ // ── Event Listeners ──────────────────────────────────────────────
134
+ /** Listen for new messages */
135
+ export function onMessage(handler) {
136
+ addHandler('message:new', handler);
137
+ return () => removeHandler('message:new', handler);
138
+ }
139
+ /** Listen for typing updates */
140
+ export function onTyping(handler) {
141
+ addHandler('typing:update', handler);
142
+ return () => removeHandler('typing:update', handler);
143
+ }
144
+ /** Listen for presence changes */
145
+ export function onPresence(handler) {
146
+ addHandler('presence:changed', handler);
147
+ return () => removeHandler('presence:changed', handler);
148
+ }
149
+ /** Listen for bulk presence sync response */
150
+ export function onPresenceSync(handler) {
151
+ addHandler('presence:sync', handler);
152
+ return () => removeHandler('presence:sync', handler);
153
+ }
73
154
  /** Listen for reaction updates */
74
155
  export function onReactionUpdate(handler) {
75
156
  addHandler('message:reaction-update', handler);
@@ -85,6 +166,17 @@ export function onMessageDeleted(handler) {
85
166
  addHandler('message:deleted', handler);
86
167
  return () => removeHandler('message:deleted', handler);
87
168
  }
169
+ /** Listen for read receipts */
170
+ export function onReadReceipt(handler) {
171
+ addHandler('read-receipt:update', handler);
172
+ return () => removeHandler('read-receipt:update', handler);
173
+ }
174
+ /** Listen for lazy-loaded conversation messages */
175
+ export function onConversationMessages(handler) {
176
+ addHandler('conversation:messages', handler);
177
+ return () => removeHandler('conversation:messages', handler);
178
+ }
179
+ // ── File Uploads & Link Previews ─────────────────────────────────
88
180
  /** Upload a file (returns attachment metadata) */
89
181
  export async function uploadFile(file, filename, encrypted = false) {
90
182
  const res = await fetch('/__nk_comm/upload', {
@@ -112,45 +204,13 @@ export async function fetchLinkPreviews(text) {
112
204
  const data = await res.json();
113
205
  return data.previews || [];
114
206
  }
115
- /** Start typing indicator */
116
- export function startTyping(conversationId) {
117
- emit('typing:start', { conversationId });
118
- }
119
- /** Stop typing indicator */
120
- export function stopTyping(conversationId) {
121
- emit('typing:stop', { conversationId });
122
- }
123
- /** Update presence status */
124
- export function updatePresence(status) {
125
- emit('presence:update', { status });
126
- }
127
- /** Listen for new messages */
128
- export function onMessage(handler) {
129
- addHandler('message:new', handler);
130
- return () => removeHandler('message:new', handler);
131
- }
132
- /** Listen for typing updates */
133
- export function onTyping(handler) {
134
- addHandler('typing:update', handler);
135
- return () => removeHandler('typing:update', handler);
136
- }
137
- /** Listen for presence changes */
138
- export function onPresence(handler) {
139
- addHandler('presence:changed', handler);
140
- return () => removeHandler('presence:changed', handler);
141
- }
142
- /** Listen for read receipts */
143
- export function onReadReceipt(handler) {
144
- addHandler('read-receipt:update', handler);
145
- return () => removeHandler('read-receipt:update', handler);
146
- }
147
- // ── Calls ─────────────────────────────────────────────────────────
207
+ // ── Calls ────────────────────────────────────────────────────────
148
208
  /** Listen for incoming calls */
149
209
  export function onIncomingCall(handler) {
150
210
  addHandler('call:incoming', handler);
151
211
  return () => removeHandler('call:incoming', handler);
152
212
  }
153
- /** Listen for call state changes (initiating, ringing, connecting, connected, ended) */
213
+ /** Listen for call state changes */
154
214
  export function onCallStateChanged(handler) {
155
215
  addHandler('call:state-changed', handler);
156
216
  return () => removeHandler('call:state-changed', handler);
@@ -171,22 +231,22 @@ export function onMediaChanged(handler) {
171
231
  return () => removeHandler('call:media-changed', handler);
172
232
  }
173
233
  /** Initiate an audio or video call */
174
- export function initiateCall(conversationId, type, calleeIds) {
175
- emit('call:initiate', { conversationId, type, calleeIds });
234
+ export function initiateCall(conversationId, type, calleeIds, caller) {
235
+ emit('call:initiate', { conversationId, type, calleeIds, ...caller });
176
236
  }
177
237
  /** Respond to an incoming call */
178
238
  export function respondToCall(callId, action) {
179
239
  emit('call:respond', { callId, action });
180
240
  }
181
241
  /** Hang up an active call */
182
- export function hangup(callId, reason = 'completed') {
183
- emit('call:hangup', { callId, reason });
242
+ export function hangup(callId, reason = 'completed', duration) {
243
+ emit('call:hangup', { callId, reason, ...(duration ? { duration } : {}) });
184
244
  }
185
245
  /** Toggle audio/video/screenshare during a call */
186
246
  export function toggleMedia(callId, opts) {
187
247
  emit('call:media-toggle', { callId, ...opts });
188
248
  }
189
- // ── WebRTC Signaling ──────────────────────────────────────────────
249
+ // ── WebRTC Signaling ─────────────────────────────────────────────
190
250
  /** Send SDP offer to a peer */
191
251
  export function sendOffer(callId, toUserId, sdp) {
192
252
  emit('signal:offer', { callId, fromUserId: '', toUserId, type: 'offer', sdp });
@@ -170,7 +170,7 @@ export class NkRouter {
170
170
  es.onmessage = (e) => {
171
171
  const pageEl = this.findPageElement(match.route.tagName);
172
172
  if (pageEl)
173
- pageEl.liveData = JSON.parse(e.data);
173
+ this.spreadData(pageEl, JSON.parse(e.data));
174
174
  };
175
175
  this.subscriptions.push(es);
176
176
  }
@@ -204,7 +204,7 @@ export class NkRouter {
204
204
  socket.on('nk:data', (data) => {
205
205
  const pageEl = this.findPageElement(match.route.tagName);
206
206
  if (pageEl) {
207
- pageEl.liveData = data;
207
+ this.spreadData(pageEl, data);
208
208
  injectEmit();
209
209
  }
210
210
  });
@@ -217,7 +217,7 @@ export class NkRouter {
217
217
  es.onmessage = (e) => {
218
218
  const layoutEl = this.outlet?.querySelector(layout.tagName);
219
219
  if (layoutEl)
220
- layoutEl.liveData = JSON.parse(e.data);
220
+ this.spreadData(layoutEl, JSON.parse(e.data));
221
221
  };
222
222
  this.subscriptions.push(es);
223
223
  }
@@ -66,7 +66,14 @@ export declare class GroupWebRTCManager {
66
66
  getRemoteStream(userId: string): MediaStream | null;
67
67
  getRemoteStreams(): Map<string, MediaStream>;
68
68
  /** Acquire local media — call once before adding peers */
69
- startLocalMedia(video?: boolean, audio?: boolean): Promise<MediaStream>;
69
+ startLocalMedia(video?: boolean, audio?: boolean, deviceIds?: {
70
+ audioInputId?: string | null;
71
+ videoInputId?: string | null;
72
+ }): Promise<MediaStream>;
73
+ /** Replace the audio track on all peer connections (for switching mic mid-call) */
74
+ replaceAudioTrack(newTrack: MediaStreamTrack): Promise<void>;
75
+ /** Replace the video track on all peer connections (for switching camera mid-call) */
76
+ replaceVideoTrack(newTrack: MediaStreamTrack): Promise<void>;
70
77
  /** Create a peer connection for a remote user and optionally create an offer */
71
78
  addPeer(userId: string, isCaller: boolean): Promise<string | void>;
72
79
  /** Remove and close a peer connection */