dolphin-client 1.0.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.
@@ -0,0 +1,2042 @@
1
+ var DolphinModule = (() => {
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DolphinClient: () => DolphinClient
24
+ });
25
+
26
+ // src/api.ts
27
+ var APIHandler = class {
28
+ client;
29
+ /** @param {DolphinClient} client */
30
+ constructor(client) {
31
+ this.client = client;
32
+ return this._createProxy([]);
33
+ }
34
+ /** @private */
35
+ _createProxy(pathParts) {
36
+ const joined = pathParts.join("/");
37
+ const target = (options) => this.request("GET", joined, null, options);
38
+ target.get = (pathOrOptions, options) => typeof pathOrOptions === "string" ? this.request("GET", pathOrOptions, null, options) : this.request("GET", joined, null, pathOrOptions);
39
+ target.post = (pathOrBody, bodyOrOptions, options) => typeof pathOrBody === "string" ? this.request("POST", pathOrBody, bodyOrOptions, options) : this.request("POST", joined, pathOrBody, bodyOrOptions);
40
+ target.put = (pathOrBody, bodyOrOptions, options) => typeof pathOrBody === "string" ? this.request("PUT", pathOrBody, bodyOrOptions, options) : this.request("PUT", joined, pathOrBody, bodyOrOptions);
41
+ target.patch = (pathOrBody, bodyOrOptions, options) => typeof pathOrBody === "string" ? this.request("PATCH", pathOrBody, bodyOrOptions, options) : this.request("PATCH", joined, pathOrBody, bodyOrOptions);
42
+ target.del = (pathOrOptions, options) => typeof pathOrOptions === "string" ? this.request("DELETE", pathOrOptions, null, options) : this.request("DELETE", joined, null, pathOrOptions);
43
+ target.request = (method, subPath, body, options) => {
44
+ const finalPath = subPath ? `${joined}/${subPath.startsWith("/") ? subPath.slice(1) : subPath}` : joined;
45
+ return this.request(method, finalPath, body, options);
46
+ };
47
+ target.requestDirect = (method, path, body, options) => {
48
+ return this.requestDirect(method, path, body, options);
49
+ };
50
+ const methods = ["get", "post", "put", "patch", "del", "request", "requestDirect"];
51
+ return new Proxy(target, {
52
+ get: (t, prop) => {
53
+ if (typeof prop === "string" && !methods.includes(prop)) {
54
+ return this._createProxy([...pathParts, prop]);
55
+ }
56
+ return t[prop];
57
+ }
58
+ });
59
+ }
60
+ /**
61
+ * Intercept request for offline-first caching and queuing.
62
+ */
63
+ async request(method, path, body = null, options = {}) {
64
+ if (this.client.offline) {
65
+ const isOnline = this.client.offline.isOnline;
66
+ const cacheKey = `${method.toUpperCase()}:${path}`;
67
+ if (method.toUpperCase() === "GET") {
68
+ if (isOnline) {
69
+ try {
70
+ const result = await this.requestDirect(method, path, body, options);
71
+ await this.client.offline.setCache(cacheKey, result);
72
+ return result;
73
+ } catch (err) {
74
+ const cached = await this.client.offline.getCache(cacheKey);
75
+ if (cached !== void 0 && cached !== null) {
76
+ return cached;
77
+ }
78
+ throw err;
79
+ }
80
+ } else {
81
+ const cached = await this.client.offline.getCache(cacheKey);
82
+ if (cached !== void 0 && cached !== null) {
83
+ return cached;
84
+ }
85
+ throw { status: 503, data: { error: "Offline, no cache available" } };
86
+ }
87
+ } else {
88
+ if (isOnline) {
89
+ return this.requestDirect(method, path, body, options);
90
+ } else {
91
+ await this.client.offline.queueMutation(method, path, body);
92
+ this.client._dispatch("offline:queued", { method, path, body });
93
+ return { success: true, offline: true, message: "Mutation queued offline" };
94
+ }
95
+ }
96
+ }
97
+ return this.requestDirect(method, path, body, options);
98
+ }
99
+ /**
100
+ * Make an HTTP request with timeout + auto token refresh.
101
+ * @param {string} method
102
+ * @param {string} path
103
+ * @param {any} [body]
104
+ * @param {RequestInit} [options]
105
+ * @param {boolean} [_isRetry=false] — internal: prevent infinite refresh loop
106
+ * @returns {Promise<any>}
107
+ */
108
+ async requestDirect(method, path, body = null, options = {}) {
109
+ const _isRetry = options._isRetry === true;
110
+ const url = `${this.client.httpUrl}${path.startsWith("/") ? path : "/" + path}`;
111
+ const controller = new AbortController();
112
+ const timeoutId = setTimeout(
113
+ () => controller.abort(),
114
+ this.client.options.timeout
115
+ );
116
+ const headers = {
117
+ "Content-Type": "application/json",
118
+ ...options.headers || {}
119
+ };
120
+ if (this.client.accessToken) {
121
+ headers["Authorization"] = `Bearer ${this.client.accessToken}`;
122
+ }
123
+ const fetchOptions = { ...options };
124
+ delete fetchOptions._isRetry;
125
+ try {
126
+ const response = await fetch(url, {
127
+ method,
128
+ headers,
129
+ signal: controller.signal,
130
+ ...body ? { body: JSON.stringify(body) } : {},
131
+ ...fetchOptions
132
+ });
133
+ clearTimeout(timeoutId);
134
+ if (response.status === 401 && !_isRetry && this.client.options.autoRefreshToken) {
135
+ const refreshed = await this.client.auth._silentRefresh();
136
+ if (refreshed) {
137
+ return this.request(method, path, body, { ...options, _isRetry: true });
138
+ }
139
+ }
140
+ const contentType = response.headers.get("content-type") || "";
141
+ const data = contentType.includes("application/json") ? await response.json() : await response.text();
142
+ if (!response.ok) throw { status: response.status, data };
143
+ if (data && typeof data === "object") {
144
+ if (data.accessToken) {
145
+ this.client.setToken(data.accessToken);
146
+ if (data.user) this.client.auth.user = data.user;
147
+ }
148
+ }
149
+ if (this.client.options.autoBroadcast && ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase())) {
150
+ const cleanPath = path.startsWith("/") ? path.substring(1) : path;
151
+ this.client.publish(cleanPath, { method: method.toUpperCase(), payload: body, result: data });
152
+ }
153
+ return data;
154
+ } catch (err) {
155
+ clearTimeout(timeoutId);
156
+ if (err.name === "AbortError") {
157
+ throw { status: 408, data: { error: "Request timed out" } };
158
+ }
159
+ throw err;
160
+ }
161
+ }
162
+ };
163
+
164
+ // src/auth.ts
165
+ var AuthHandler = class {
166
+ client;
167
+ user;
168
+ _refreshing;
169
+ /** @param {DolphinClient} client */
170
+ constructor(client) {
171
+ this.client = client;
172
+ this.user = null;
173
+ this._refreshing = false;
174
+ }
175
+ /**
176
+ * Login with email + password.
177
+ * @param {string} email
178
+ * @param {string} password
179
+ */
180
+ async login(email, password) {
181
+ const res = await this.client.api.post("/api/auth/login", { email, password });
182
+ if (res.accessToken) {
183
+ this.client.setToken(res.accessToken);
184
+ this.user = res.user || null;
185
+ }
186
+ return res;
187
+ }
188
+ /**
189
+ * Register a new account.
190
+ * @param {{ email: string, password: string, [key: string]: any }} data
191
+ */
192
+ async register(data) {
193
+ return this.client.api.post("/api/auth/register", data);
194
+ }
195
+ /** Get current user profile. */
196
+ async me() {
197
+ const res = await this.client.api.get("/api/auth/me");
198
+ if (res.success) this.user = res.data;
199
+ return res;
200
+ }
201
+ /** Logout and clear token. */
202
+ async logout() {
203
+ try {
204
+ await this.client.api.post("/api/auth/logout");
205
+ } catch {
206
+ }
207
+ this.client.setToken(null);
208
+ this.user = null;
209
+ }
210
+ /**
211
+ * Manually refresh the access token using the httpOnly refresh-token cookie.
212
+ * Called automatically on 401 if autoRefreshToken is enabled.
213
+ * @returns {Promise<boolean>} — true if refresh succeeded
214
+ */
215
+ async refresh() {
216
+ return this._silentRefresh();
217
+ }
218
+ /** @private */
219
+ async _silentRefresh() {
220
+ if (this._refreshing) return false;
221
+ this._refreshing = true;
222
+ try {
223
+ const res = await this.client.api.post("/api/auth/refresh", null, { _isRetry: true });
224
+ if (res.accessToken) {
225
+ this.client.setToken(res.accessToken);
226
+ return true;
227
+ }
228
+ return false;
229
+ } catch {
230
+ this.client.setToken(null);
231
+ return false;
232
+ } finally {
233
+ this._refreshing = false;
234
+ }
235
+ }
236
+ /**
237
+ * Verify a 2FA TOTP code after login.
238
+ * @param {string} code — 6-digit TOTP code
239
+ * @param {string} [email] — email (if not already in user)
240
+ */
241
+ async verify2FA(code, email) {
242
+ const payload = {
243
+ code,
244
+ email: email || this.user?.email
245
+ };
246
+ const res = await this.client.api.post("/api/auth/2fa/verify", payload);
247
+ if (res.accessToken) {
248
+ this.client.setToken(res.accessToken);
249
+ if (res.user) this.user = res.user;
250
+ }
251
+ return res;
252
+ }
253
+ /**
254
+ * Enable 2FA — returns QR code URL and secret.
255
+ */
256
+ async enable2FA() {
257
+ return this.client.api.post("/api/auth/2fa/enable");
258
+ }
259
+ /**
260
+ * Disable 2FA.
261
+ * @param {string} code — current TOTP code to confirm
262
+ */
263
+ async disable2FA(code) {
264
+ return this.client.api.post("/api/auth/2fa/disable", { code });
265
+ }
266
+ /**
267
+ * Request a password reset email.
268
+ * @param {string} email
269
+ */
270
+ async forgotPassword(email) {
271
+ return this.client.api.post("/api/auth/forgot-password", { email });
272
+ }
273
+ /**
274
+ * Reset password using the token from email.
275
+ * @param {string} token
276
+ * @param {string} newPassword
277
+ */
278
+ async resetPassword(token, newPassword) {
279
+ return this.client.api.post("/api/auth/reset-password", { token, newPassword });
280
+ }
281
+ };
282
+
283
+ // src/store.ts
284
+ var DolphinStore = class {
285
+ client;
286
+ data;
287
+ listeners;
288
+ subscribed;
289
+ /** @param {DolphinClient} client */
290
+ constructor(client) {
291
+ this.client = client;
292
+ this.data = /* @__PURE__ */ new Map();
293
+ this.listeners = /* @__PURE__ */ new Set();
294
+ this.subscribed = /* @__PURE__ */ new Set();
295
+ return new Proxy(this, {
296
+ get: (target, prop) => {
297
+ if (prop in target) return target[prop];
298
+ if (typeof prop === "string") return this._getCollection(prop);
299
+ }
300
+ });
301
+ }
302
+ /** @private */
303
+ _getCollection(name) {
304
+ if (!this.data.has(name)) {
305
+ const collection = {
306
+ _rawItems: [],
307
+ items: [],
308
+ loading: true,
309
+ error: null,
310
+ success: false,
311
+ _filter: null,
312
+ _sort: null,
313
+ where: (fn) => {
314
+ collection._filter = fn;
315
+ this._applyTransform(collection);
316
+ return collection;
317
+ },
318
+ orderBy: (key, direction = "asc") => {
319
+ collection._sort = { key, direction };
320
+ this._applyTransform(collection);
321
+ return collection;
322
+ },
323
+ reset: () => {
324
+ collection._filter = null;
325
+ collection._sort = null;
326
+ this._applyTransform(collection);
327
+ return collection;
328
+ }
329
+ };
330
+ this.data.set(name, collection);
331
+ this._fetchAndSync(name);
332
+ }
333
+ return this.data.get(name);
334
+ }
335
+ /** @private */
336
+ async _fetchAndSync(name) {
337
+ const state = this.data.get(name);
338
+ try {
339
+ const res = await this.client.api.get(`/${name.toLowerCase()}`);
340
+ state._rawItems = Array.isArray(res) ? res : res.data || [];
341
+ state.loading = false;
342
+ state.success = true;
343
+ state.error = null;
344
+ this._applyTransform(state);
345
+ if (!this.subscribed.has(name)) {
346
+ this.client.subscribe(`db:sync/${name.toLowerCase()}`, (update) => {
347
+ this._handleRemoteUpdate(name, update);
348
+ });
349
+ this.subscribed.add(name);
350
+ }
351
+ } catch (e) {
352
+ state.loading = false;
353
+ state.success = false;
354
+ state.error = e.data?.error || e.message || "Fetch failed";
355
+ this._notify();
356
+ }
357
+ }
358
+ /** @private */
359
+ _applyTransform(state) {
360
+ let result = [...state._rawItems];
361
+ if (state._filter) result = result.filter(state._filter);
362
+ if (state._sort) {
363
+ const { key, direction } = state._sort;
364
+ result.sort((a, b) => {
365
+ if (a[key] === b[key]) return 0;
366
+ return (a[key] > b[key] ? 1 : -1) * (direction === "asc" ? 1 : -1);
367
+ });
368
+ }
369
+ state.items = result;
370
+ this._notify();
371
+ }
372
+ /** @private */
373
+ _handleRemoteUpdate(collection, update) {
374
+ const state = this.data.get(collection);
375
+ if (!state) return;
376
+ const { type, data } = update;
377
+ let items = state._rawItems;
378
+ if (type === "create") {
379
+ items = [...items, data];
380
+ } else if (type === "update") {
381
+ items = items.map((i) => i.id === data.id || i._id === data._id ? { ...i, ...data } : i);
382
+ } else if (type === "delete") {
383
+ items = items.filter((i) => {
384
+ if (data.id != null && i.id === data.id) return false;
385
+ if (data._id != null && i._id === data._id) return false;
386
+ return true;
387
+ });
388
+ }
389
+ state._rawItems = items;
390
+ this._applyTransform(state);
391
+ }
392
+ /** Subscribe for React useSyncExternalStore */
393
+ subscribe(listener) {
394
+ this.listeners.add(listener);
395
+ return () => this.listeners.delete(listener);
396
+ }
397
+ /** @param {string} collection */
398
+ getSnapshot(collection) {
399
+ return this.data.get(collection) || { items: [], loading: false, error: null, success: false };
400
+ }
401
+ /** @private */
402
+ _notify() {
403
+ this.listeners.forEach((l) => l());
404
+ }
405
+ };
406
+
407
+ // src/core.ts
408
+ var DolphinClient = class {
409
+ host;
410
+ httpUrl;
411
+ deviceId;
412
+ options;
413
+ socket;
414
+ storage;
415
+ accessToken;
416
+ api;
417
+ auth;
418
+ store;
419
+ handlers;
420
+ signalHandlers;
421
+ fileHandlers;
422
+ _offlineQueue;
423
+ reconnectAttempts;
424
+ _attachedListeners;
425
+ constructor(url = "", deviceId = "", options = {}) {
426
+ if (!url && typeof window !== "undefined") url = window.location.host;
427
+ let protocol = "http:";
428
+ if (url.startsWith("https://")) protocol = "https:";
429
+ else if (url.startsWith("http://")) protocol = "http:";
430
+ else if (typeof window !== "undefined") protocol = window.location.protocol;
431
+ this.host = (url || "localhost").replace(/\/$/, "").replace(/^https?:\/\//, "");
432
+ this.httpUrl = `${protocol}//${this.host}`;
433
+ this.deviceId = deviceId || "web_" + Math.random().toString(36).substr(2, 8);
434
+ this.options = {
435
+ timeout: 15e3,
436
+ chunkSize: 65536,
437
+ // 64 KB
438
+ maxReconnect: 5,
439
+ autoRefreshToken: true,
440
+ ...options
441
+ };
442
+ this.socket = null;
443
+ this.storage = typeof localStorage !== "undefined" ? localStorage : {
444
+ getItem: () => null,
445
+ setItem: () => {
446
+ },
447
+ removeItem: () => {
448
+ }
449
+ };
450
+ this.accessToken = this.storage.getItem("dolphin_token");
451
+ this.api = new APIHandler(this);
452
+ this.auth = new AuthHandler(this);
453
+ this.store = new DolphinStore(this);
454
+ this.handlers = /* @__PURE__ */ new Map();
455
+ this.signalHandlers = /* @__PURE__ */ new Set();
456
+ this.fileHandlers = /* @__PURE__ */ new Set();
457
+ this._offlineQueue = [];
458
+ this.reconnectAttempts = 0;
459
+ this._attachedListeners = [];
460
+ if (typeof window !== "undefined" && typeof this._initDOMBinding === "function") {
461
+ this._initDOMBinding();
462
+ }
463
+ if (typeof this._initOffline === "function") {
464
+ this._initOffline();
465
+ }
466
+ if (typeof this._initA11y === "function") {
467
+ this._initA11y();
468
+ }
469
+ if (typeof this._initI18n === "function") {
470
+ this._initI18n();
471
+ }
472
+ if (typeof this._initDragDrop === "function") {
473
+ this._initDragDrop();
474
+ }
475
+ if (typeof this._initCollab === "function") {
476
+ this._initCollab();
477
+ }
478
+ }
479
+ /** Save or clear the access token */
480
+ setToken(token) {
481
+ this.accessToken = token;
482
+ token ? this.storage.setItem("dolphin_token", token) : this.storage.removeItem("dolphin_token");
483
+ }
484
+ // ── WebSocket ─────────────────────────────────────────────────────────────
485
+ /** Connect to the Dolphin realtime server */
486
+ async connect() {
487
+ return new Promise((resolve, reject) => {
488
+ const protocol = this.httpUrl.startsWith("https") ? "wss:" : "ws:";
489
+ const wsUrl = `${protocol}//${this.host}/realtime?deviceId=${this.deviceId}`;
490
+ console.log(`[Dolphin] Connecting to ${wsUrl}...`);
491
+ this.socket = new WebSocket(wsUrl);
492
+ this.socket.onopen = () => {
493
+ console.log(`[Dolphin] Connected as "${this.deviceId}" \u{1F42C}`);
494
+ this.reconnectAttempts = 0;
495
+ this._flushOfflineQueue();
496
+ resolve();
497
+ };
498
+ this.socket.onmessage = (ev) => this._handleMessage(ev.data);
499
+ this.socket.onclose = () => {
500
+ console.warn("[Dolphin] Connection closed");
501
+ this._maybeReconnect();
502
+ };
503
+ this.socket.onerror = (err) => {
504
+ console.error("[Dolphin] WebSocket error:", err);
505
+ reject(err);
506
+ };
507
+ });
508
+ }
509
+ /** Disconnect cleanly */
510
+ disconnect() {
511
+ if (this.socket) {
512
+ this.socket.onclose = null;
513
+ this.socket.close();
514
+ this.socket = null;
515
+ }
516
+ this.cleanupDomListeners();
517
+ }
518
+ /** @private */
519
+ _handleMessage(data) {
520
+ try {
521
+ const msg = JSON.parse(data);
522
+ if (msg.type && msg.from && (msg.to === this.deviceId || msg.to === "all")) {
523
+ if (msg.msgId && msg.type !== "ACK") this._sendAck(msg.from, msg.msgId);
524
+ this.signalHandlers.forEach((h) => h(msg));
525
+ }
526
+ if (msg.type === "FILE_AVAILABLE") {
527
+ this.fileHandlers.forEach((h) => h(msg));
528
+ }
529
+ if (msg.type === "FILE_CHUNK") {
530
+ this.saveFileProgress(msg.fileId, msg.chunkIndex);
531
+ this._dispatch("file:chunk", msg);
532
+ this._dispatch(`file:chunk/${msg.fileId}`, msg);
533
+ }
534
+ if (msg.type === "FILE_UPLOAD_ACK") {
535
+ this._dispatch(`file:upload:ack/${msg.fileId}`, msg);
536
+ }
537
+ if (msg.type === "PULL_RESPONSE") {
538
+ this._dispatch("pull:response", msg.payload, msg.topic);
539
+ this._dispatch(`pull:response/${msg.topic}`, msg.payload, msg.topic);
540
+ }
541
+ if (msg.topic && msg.payload !== void 0) {
542
+ this.handlers.forEach((cbs, pattern) => {
543
+ if (this._matchTopic(pattern, msg.topic)) {
544
+ cbs.forEach((cb) => cb(msg.payload, msg.topic));
545
+ }
546
+ });
547
+ }
548
+ } catch {
549
+ this._dispatch("raw", data);
550
+ }
551
+ }
552
+ /** @private */
553
+ _dispatch(pattern, payload, topic) {
554
+ const cbs = this.handlers.get(pattern);
555
+ if (cbs) cbs.forEach((cb) => cb(payload, topic || pattern));
556
+ }
557
+ /** @private */
558
+ _sendRaw(msg) {
559
+ const str = typeof msg === "string" ? msg : JSON.stringify(msg);
560
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
561
+ this.socket.send(str);
562
+ } else {
563
+ if (this._offlineQueue.length < 100) this._offlineQueue.push(str);
564
+ }
565
+ }
566
+ /** Flush buffered messages after reconnect @private */
567
+ _flushOfflineQueue() {
568
+ while (this._offlineQueue.length > 0) {
569
+ const msg = this._offlineQueue.shift();
570
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
571
+ this.socket.send(msg);
572
+ }
573
+ }
574
+ }
575
+ /** @private */
576
+ _sendAck(to, msgId) {
577
+ this._sendRaw({ type: "ACK", from: this.deviceId, to, data: { ackId: msgId }, timestamp: Date.now() });
578
+ }
579
+ /** MQTT wildcard topic matching @private */
580
+ _matchTopic(pattern, topic) {
581
+ if (pattern === topic || pattern === "#") return true;
582
+ const pp = pattern.split("/");
583
+ const tp = topic.split("/");
584
+ if (pp.length !== tp.length && !pattern.includes("#")) return false;
585
+ for (let i = 0; i < pp.length; i++) {
586
+ if (pp[i] === "#") return true;
587
+ if (pp[i] !== "+" && pp[i] !== tp[i]) return false;
588
+ }
589
+ return pp.length === tp.length;
590
+ }
591
+ /** @private */
592
+ _maybeReconnect() {
593
+ if (this.reconnectAttempts < this.options.maxReconnect) {
594
+ this.reconnectAttempts++;
595
+ const delay = Math.pow(2, this.reconnectAttempts) * 1e3;
596
+ console.log(`[Dolphin] Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts})...`);
597
+ setTimeout(() => this.connect().catch(() => {
598
+ }), delay);
599
+ } else {
600
+ console.error("[Dolphin] Max reconnect attempts reached.");
601
+ }
602
+ }
603
+ // ── Pub/Sub ───────────────────────────────────────────────────────────────
604
+ /**
605
+ * Subscribe to a topic (MQTT wildcards supported: + and #).
606
+ * @param {string} topic
607
+ * @param {TopicCallback} callback
608
+ */
609
+ subscribe(topic, callback) {
610
+ if (!this.handlers.has(topic)) {
611
+ this.handlers.set(topic, /* @__PURE__ */ new Set());
612
+ this._sendRaw({ type: "sub", topic });
613
+ }
614
+ this.handlers.get(topic).add(callback);
615
+ }
616
+ /**
617
+ * Unsubscribe from a topic.
618
+ * @param {string} topic
619
+ * @param {TopicCallback} callback
620
+ */
621
+ unsubscribe(topic, callback) {
622
+ if (this.handlers.has(topic)) {
623
+ const cbs = this.handlers.get(topic);
624
+ cbs.delete(callback);
625
+ if (cbs.size === 0) {
626
+ this.handlers.delete(topic);
627
+ this._sendRaw({ type: "unsub", topic });
628
+ }
629
+ }
630
+ }
631
+ /**
632
+ * Publish a message to a topic. Queued if offline.
633
+ * @param {string} topic
634
+ * @param {any} payload
635
+ */
636
+ publish(topic, payload) {
637
+ this._sendRaw({ topic, payload });
638
+ }
639
+ /**
640
+ * High-frequency data push (IoT sensors).
641
+ * @param {string} topic
642
+ * @param {any} payload
643
+ */
644
+ pubPush(topic, payload) {
645
+ this._sendRaw({ type: "pub", topic, payload });
646
+ }
647
+ /**
648
+ * Request historical data from a topic.
649
+ * @param {string} topic
650
+ * @param {number} [count=10]
651
+ */
652
+ subPull(topic, count = 10) {
653
+ this._sendRaw({ type: "PULL_REQUEST", topic, count });
654
+ }
655
+ // ── File Transfer ─────────────────────────────────────────────────────────
656
+ /**
657
+ * Upload a file to the server in chunks.
658
+ * @param {string} fileId
659
+ * @param {Blob|ArrayBuffer|Uint8Array} fileData
660
+ * @param {string} [filename]
661
+ * @param {function(number): void} [onProgress] — progress callback (0-100)
662
+ * @returns {Promise<void>}
663
+ */
664
+ async pubFile(fileId, fileData, filename = "", onProgress) {
665
+ let buffer;
666
+ if (fileData instanceof Blob) {
667
+ buffer = await fileData.arrayBuffer();
668
+ } else if (fileData instanceof ArrayBuffer) {
669
+ buffer = fileData;
670
+ } else {
671
+ buffer = fileData.buffer || fileData;
672
+ }
673
+ const bytes = new Uint8Array(buffer);
674
+ const chunkSize = this.options.chunkSize;
675
+ const totalChunks = Math.ceil(bytes.length / chunkSize);
676
+ this._sendRaw({
677
+ type: "FILE_UPLOAD_START",
678
+ fileId,
679
+ name: filename,
680
+ size: bytes.length,
681
+ totalChunks,
682
+ chunkSize
683
+ });
684
+ for (let i = 0; i < totalChunks; i++) {
685
+ const chunk = bytes.slice(i * chunkSize, (i + 1) * chunkSize);
686
+ const b64 = this._uint8ToBase64(chunk);
687
+ this._sendRaw({
688
+ type: "FILE_UPLOAD_CHUNK",
689
+ fileId,
690
+ chunkIndex: i,
691
+ totalChunks,
692
+ data: b64
693
+ });
694
+ if (onProgress) onProgress(Math.round((i + 1) / totalChunks * 100));
695
+ if (i % 10 === 0) await new Promise((r) => setTimeout(r, 0));
696
+ }
697
+ this._sendRaw({ type: "FILE_UPLOAD_DONE", fileId });
698
+ }
699
+ /** @private */
700
+ _uint8ToBase64(uint8) {
701
+ let binary = "";
702
+ for (let i = 0; i < uint8.length; i++) binary += String.fromCharCode(uint8[i]);
703
+ if (typeof btoa !== "undefined") return btoa(binary);
704
+ return Buffer.from(binary, "binary").toString("base64");
705
+ }
706
+ /**
707
+ * Download a file from the server by chunks.
708
+ * @param {string} fileId
709
+ * @param {number} [startChunk=0]
710
+ */
711
+ subFile(fileId, startChunk = 0) {
712
+ this._sendRaw({ type: "FILE_REQUEST", fileId, startChunk });
713
+ }
714
+ /**
715
+ * Resume a file download from saved progress.
716
+ * @param {string} fileId
717
+ */
718
+ resumeFile(fileId) {
719
+ const last = parseInt(this.storage.getItem(`dolphin_file_${fileId}`) || "-1");
720
+ this.subFile(fileId, last + 1);
721
+ }
722
+ /**
723
+ * Save download chunk progress.
724
+ * @param {string} fileId
725
+ * @param {number} chunkIndex
726
+ */
727
+ saveFileProgress(fileId, chunkIndex) {
728
+ this.storage.setItem(`dolphin_file_${fileId}`, chunkIndex.toString());
729
+ }
730
+ // ── Signaling ─────────────────────────────────────────────────────────────
731
+ /**
732
+ * @param {function(SignalMessage): void} handler
733
+ */
734
+ onSignal(handler) {
735
+ this.signalHandlers.add(handler);
736
+ }
737
+ /**
738
+ * @param {function(SignalMessage): void} handler
739
+ */
740
+ offSignal(handler) {
741
+ this.signalHandlers.delete(handler);
742
+ }
743
+ /**
744
+ * @param {function(FileMetadata): void} handler
745
+ */
746
+ onFileAvailable(handler) {
747
+ this.fileHandlers.add(handler);
748
+ }
749
+ /**
750
+ * @param {function(FileMetadata): void} handler
751
+ */
752
+ offFileAvailable(handler) {
753
+ this.fileHandlers.delete(handler);
754
+ }
755
+ addDomListener(target, event, cb) {
756
+ if (!target) return;
757
+ target.addEventListener(event, cb);
758
+ this._attachedListeners = this._attachedListeners || [];
759
+ this._attachedListeners.push({ target, event, cb });
760
+ }
761
+ cleanupDomListeners() {
762
+ if (this._attachedListeners) {
763
+ this._attachedListeners.forEach(({ target, event, cb }) => {
764
+ try {
765
+ target.removeEventListener(event, cb);
766
+ } catch {
767
+ }
768
+ });
769
+ this._attachedListeners = [];
770
+ }
771
+ }
772
+ };
773
+
774
+ // src/dom.ts
775
+ function attachDOMBinding(clientProto) {
776
+ function escapeRegExp(str) {
777
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
778
+ }
779
+ function resolveTemplate(el) {
780
+ const template = el.getAttribute("data-rt-template");
781
+ if (!template) return null;
782
+ if (typeof document !== "undefined" && !template.includes("<")) {
783
+ try {
784
+ const tempEl = document.querySelector(template);
785
+ if (tempEl) return tempEl.innerHTML;
786
+ } catch {
787
+ }
788
+ }
789
+ return template;
790
+ }
791
+ function sanitizeHTML(html) {
792
+ if (typeof document === "undefined") return html;
793
+ try {
794
+ const parser = new DOMParser();
795
+ const doc = parser.parseFromString(html, "text/html");
796
+ const body = doc.body;
797
+ const sanitizeNode = (el) => {
798
+ const tag = el.tagName.toLowerCase();
799
+ if (["script", "iframe", "object", "embed", "link", "style", "meta", "applet", "svg"].includes(tag)) {
800
+ el.parentNode?.removeChild(el);
801
+ return;
802
+ }
803
+ const attrs = el.attributes;
804
+ for (let i = attrs.length - 1; i >= 0; i--) {
805
+ const attrName = attrs[i].name.toLowerCase();
806
+ const attrVal = attrs[i].value.toLowerCase();
807
+ if (attrName.startsWith("on")) {
808
+ el.removeAttribute(attrs[i].name);
809
+ } else if (["src", "href", "data"].includes(attrName) && (attrVal.includes("javascript:") || attrVal.includes("data:text/html"))) {
810
+ el.removeAttribute(attrs[i].name);
811
+ }
812
+ }
813
+ Array.from(el.children).forEach(sanitizeNode);
814
+ };
815
+ Array.from(body.children).forEach(sanitizeNode);
816
+ return body.innerHTML;
817
+ } catch {
818
+ return html;
819
+ }
820
+ }
821
+ function diffDOM(existingNode, newNode) {
822
+ if (existingNode.nodeType !== newNode.nodeType) {
823
+ existingNode.parentNode?.replaceChild(newNode.cloneNode(true), existingNode);
824
+ return;
825
+ }
826
+ if (existingNode.nodeType === Node.TEXT_NODE) {
827
+ if (existingNode.textContent !== newNode.textContent) {
828
+ existingNode.textContent = newNode.textContent;
829
+ }
830
+ return;
831
+ }
832
+ if (existingNode.nodeType === Node.ELEMENT_NODE) {
833
+ const el1 = existingNode;
834
+ const el2 = newNode;
835
+ if (el1.tagName !== el2.tagName) {
836
+ el1.parentNode?.replaceChild(el2.cloneNode(true), el1);
837
+ return;
838
+ }
839
+ const attr1 = el1.attributes;
840
+ const attr2 = el2.attributes;
841
+ for (let i = attr1.length - 1; i >= 0; i--) {
842
+ const name = attr1[i].name;
843
+ if (!el2.hasAttribute(name)) el1.removeAttribute(name);
844
+ }
845
+ for (let i = 0; i < attr2.length; i++) {
846
+ const name = attr2[i].name;
847
+ const val = attr2[i].value;
848
+ if (el1.getAttribute(name) !== val) el1.setAttribute(name, val);
849
+ }
850
+ if (el1.tagName === "INPUT" || el1.tagName === "TEXTAREA") {
851
+ if (el1.value !== el2.value) el1.value = el2.value;
852
+ if (el1.checked !== el2.checked) el1.checked = el2.checked;
853
+ } else if (el1.tagName === "SELECT") {
854
+ if (el1.value !== el2.value) el1.value = el2.value;
855
+ }
856
+ const childs1 = Array.from(el1.childNodes);
857
+ const childs2 = Array.from(el2.childNodes);
858
+ const len1 = childs1.length;
859
+ const len2 = childs2.length;
860
+ const maxLen = Math.max(len1, len2);
861
+ for (let i = 0; i < maxLen; i++) {
862
+ if (i >= len1) {
863
+ el1.appendChild(childs2[i].cloneNode(true));
864
+ } else if (i >= len2) {
865
+ el1.removeChild(childs1[i]);
866
+ } else {
867
+ diffDOM(childs1[i], childs2[i]);
868
+ }
869
+ }
870
+ }
871
+ }
872
+ function patchDOM(parentElement, newHTML) {
873
+ if (typeof document === "undefined") return;
874
+ const temp = document.createElement(parentElement.tagName);
875
+ temp.innerHTML = newHTML;
876
+ const childs1 = Array.from(parentElement.childNodes);
877
+ const childs2 = Array.from(temp.childNodes);
878
+ const len1 = childs1.length;
879
+ const len2 = childs2.length;
880
+ const maxLen = Math.max(len1, len2);
881
+ for (let i = 0; i < maxLen; i++) {
882
+ if (i >= len1) {
883
+ parentElement.appendChild(childs2[i].cloneNode(true));
884
+ } else if (i >= len2) {
885
+ parentElement.removeChild(childs1[i]);
886
+ } else {
887
+ diffDOM(childs1[i], childs2[i]);
888
+ }
889
+ }
890
+ }
891
+ const pendingUpdates = /* @__PURE__ */ new Map();
892
+ let rafScheduled = false;
893
+ function scheduleDOMUpdate(element, newHTML) {
894
+ pendingUpdates.set(element, newHTML);
895
+ if (!rafScheduled) {
896
+ rafScheduled = true;
897
+ const scheduleFn = typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : (cb) => setTimeout(cb, 0);
898
+ scheduleFn(() => {
899
+ pendingUpdates.forEach((html, el) => {
900
+ patchDOM(el, html);
901
+ });
902
+ pendingUpdates.clear();
903
+ rafScheduled = false;
904
+ });
905
+ }
906
+ }
907
+ clientProto.setStoreState = function(storeName, key, val) {
908
+ this.uiStores = this.uiStores || /* @__PURE__ */ new Map();
909
+ if (!this.uiStores.has(storeName)) {
910
+ this.uiStores.set(storeName, {});
911
+ }
912
+ const store = this.uiStores.get(storeName);
913
+ store[key] = val;
914
+ if (typeof document !== "undefined") {
915
+ const readElements = document.querySelectorAll(`[data-store-read="${storeName}.${key}"]`);
916
+ readElements.forEach((el) => {
917
+ if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
918
+ if (el.type === "checkbox") {
919
+ el.checked = !!val;
920
+ } else {
921
+ el.value = val !== void 0 && val !== null ? val : "";
922
+ }
923
+ } else {
924
+ el.textContent = val !== void 0 && val !== null ? val : "";
925
+ }
926
+ });
927
+ }
928
+ this.publish(`store/${storeName}`, store);
929
+ };
930
+ clientProto.getStoreState = function(storeName, key) {
931
+ this.uiStores = this.uiStores || /* @__PURE__ */ new Map();
932
+ const store = this.uiStores.get(storeName);
933
+ return store ? store[key] : void 0;
934
+ };
935
+ clientProto._scanStoreBinds = function() {
936
+ if (typeof document === "undefined") return;
937
+ const writeEls = document.querySelectorAll("[data-store-write]");
938
+ writeEls.forEach((el) => {
939
+ const writeBind = el.getAttribute("data-store-write");
940
+ if (writeBind) {
941
+ const parts = writeBind.split(".");
942
+ if (parts.length === 2) {
943
+ const storeName = parts[0];
944
+ const key = parts[1];
945
+ const val = el.type === "checkbox" ? el.checked : el.value;
946
+ this.uiStores = this.uiStores || /* @__PURE__ */ new Map();
947
+ if (!this.uiStores.has(storeName)) {
948
+ this.uiStores.set(storeName, {});
949
+ }
950
+ const store = this.uiStores.get(storeName);
951
+ if (store[key] === void 0) {
952
+ store[key] = val;
953
+ }
954
+ }
955
+ }
956
+ });
957
+ const readEls = document.querySelectorAll("[data-store-read]");
958
+ readEls.forEach((el) => {
959
+ const readBind = el.getAttribute("data-store-read");
960
+ if (readBind) {
961
+ const parts = readBind.split(".");
962
+ if (parts.length === 2) {
963
+ const storeName = parts[0];
964
+ const key = parts[1];
965
+ const val = this.getStoreState(storeName, key);
966
+ if (val !== void 0 && val !== null) {
967
+ if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
968
+ if (el.type === "checkbox") {
969
+ el.checked = !!val;
970
+ } else {
971
+ el.value = val;
972
+ }
973
+ } else {
974
+ el.textContent = val;
975
+ }
976
+ }
977
+ }
978
+ }
979
+ });
980
+ };
981
+ clientProto.getClosestContext = function(element, key) {
982
+ let current = element;
983
+ while (current) {
984
+ if (current._rtContext) {
985
+ const ctx = current._rtContext;
986
+ if (key) return ctx[key];
987
+ return ctx;
988
+ }
989
+ current = current.parentElement;
990
+ }
991
+ return null;
992
+ };
993
+ clientProto._initDOMBinding = function() {
994
+ if (this._domInitialized) return;
995
+ this._domInitialized = true;
996
+ const PUSH_EVENTS = ["input", "change", "keyup", "paste", "blur"];
997
+ const debounceTimers = /* @__PURE__ */ new Map();
998
+ PUSH_EVENTS.forEach((evtName) => {
999
+ this.addDomListener(document, evtName, (e) => {
1000
+ if (!e.target || !e.target.getAttribute) return;
1001
+ const writeBind = e.target.getAttribute("data-store-write");
1002
+ if (writeBind) {
1003
+ const parts = writeBind.split(".");
1004
+ if (parts.length === 2) {
1005
+ const storeName = parts[0];
1006
+ const key = parts[1];
1007
+ const val = e.target.type === "checkbox" ? e.target.checked : e.target.value;
1008
+ this.setStoreState(storeName, key, val);
1009
+ }
1010
+ }
1011
+ const rules = e.target.getAttribute("data-rt-validate");
1012
+ const name = e.target.name;
1013
+ if (rules && name && typeof this.validateField === "function") {
1014
+ const form = e.target.closest("form");
1015
+ const formValues = form ? Object.fromEntries(new FormData(form).entries()) : {};
1016
+ const errorMsg = this.validateField(e.target.value, rules, formValues);
1017
+ if (errorMsg) {
1018
+ e.target.classList.add("invalid");
1019
+ this.publish(`errors/${name}`, errorMsg);
1020
+ } else {
1021
+ e.target.classList.remove("invalid");
1022
+ this.publish(`errors/${name}`, "");
1023
+ }
1024
+ }
1025
+ const topic = e.target.getAttribute("data-rt-push");
1026
+ if (topic) {
1027
+ const debounceVal = e.target.getAttribute("data-rt-debounce");
1028
+ const waitMs = debounceVal ? parseInt(debounceVal, 10) : 0;
1029
+ const triggerPush = () => {
1030
+ const payload = { name: e.target.name, value: e.target.value };
1031
+ this.pubPush(topic, payload);
1032
+ };
1033
+ if (waitMs > 0) {
1034
+ if (debounceTimers.has(e.target)) {
1035
+ clearTimeout(debounceTimers.get(e.target));
1036
+ }
1037
+ const timer = setTimeout(triggerPush, waitMs);
1038
+ debounceTimers.set(e.target, timer);
1039
+ } else {
1040
+ triggerPush();
1041
+ }
1042
+ }
1043
+ });
1044
+ });
1045
+ this.addDomListener(document, "submit", async (e) => {
1046
+ if (!e.target || !e.target.getAttribute) return;
1047
+ const rtTopic = e.target.getAttribute("data-rt-submit");
1048
+ const apiTarget = e.target.getAttribute("data-api-submit");
1049
+ if (rtTopic || apiTarget) {
1050
+ const validatedInputs = e.target.querySelectorAll("[data-rt-validate]");
1051
+ let formIsValid = true;
1052
+ if (validatedInputs.length > 0 && typeof this.validateField === "function") {
1053
+ const formValues = Object.fromEntries(new FormData(e.target).entries());
1054
+ validatedInputs.forEach((inputEl) => {
1055
+ const rules = inputEl.getAttribute("data-rt-validate");
1056
+ const name = inputEl.name;
1057
+ if (rules && name) {
1058
+ const errorMsg = this.validateField(inputEl.value, rules, formValues);
1059
+ if (errorMsg) {
1060
+ formIsValid = false;
1061
+ inputEl.classList.add("invalid");
1062
+ this.publish(`errors/${name}`, errorMsg);
1063
+ } else {
1064
+ inputEl.classList.remove("invalid");
1065
+ this.publish(`errors/${name}`, "");
1066
+ }
1067
+ }
1068
+ });
1069
+ }
1070
+ if (!formIsValid) {
1071
+ e.preventDefault();
1072
+ e.stopPropagation();
1073
+ return;
1074
+ }
1075
+ e.preventDefault();
1076
+ const parentCtx = this.getClosestContext(e.target) || {};
1077
+ const formData = new FormData(e.target);
1078
+ const data = Object.fromEntries(formData.entries());
1079
+ if (rtTopic) {
1080
+ let resolvedTopic = rtTopic;
1081
+ for (const k in parentCtx) {
1082
+ const escapedK = escapeRegExp(k);
1083
+ resolvedTopic = resolvedTopic.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), parentCtx[k] !== void 0 && parentCtx[k] !== null ? parentCtx[k] : "");
1084
+ }
1085
+ this.publish(resolvedTopic, data);
1086
+ } else if (apiTarget) {
1087
+ let resolvedTarget = apiTarget;
1088
+ for (const k in parentCtx) {
1089
+ const escapedK = escapeRegExp(k);
1090
+ resolvedTarget = resolvedTarget.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), parentCtx[k] !== void 0 && parentCtx[k] !== null ? parentCtx[k] : "");
1091
+ }
1092
+ const parts = resolvedTarget.trim().split(" ");
1093
+ const method = parts.length > 1 ? parts[0].toUpperCase() : "POST";
1094
+ const path = parts.length > 1 ? parts[1] : parts[0];
1095
+ try {
1096
+ const result = await this.api.request(method, path, data);
1097
+ const resultBind = e.target.getAttribute("data-api-result");
1098
+ if (resultBind) this._updateDOM(resultBind, result);
1099
+ const redirect = e.target.getAttribute("data-api-redirect");
1100
+ if (redirect) window.location.href = redirect;
1101
+ if (e.target.hasAttribute("data-api-reload")) window.location.reload();
1102
+ } catch (err) {
1103
+ console.error("[Dolphin] API Submit Error:", err);
1104
+ }
1105
+ }
1106
+ }
1107
+ });
1108
+ const INTERACTION_EVENTS = ["click", "change", "submit", "input", "keydown", "keyup", "dblclick", "focus", "blur", "mouseenter", "mouseleave"];
1109
+ INTERACTION_EVENTS.forEach((evtName) => {
1110
+ this.addDomListener(document, evtName, async (e) => {
1111
+ if (!e.target || !e.target.closest) return;
1112
+ const rtBtn = e.target.closest(`[data-rt-${evtName}]`);
1113
+ const apiBtn = e.target.closest(`[data-api-${evtName}]`);
1114
+ if (rtBtn) {
1115
+ if (evtName === "submit") e.preventDefault();
1116
+ const topic = rtBtn.getAttribute(`data-rt-${evtName}`);
1117
+ const actionData = rtBtn.getAttribute("data-rt-payload");
1118
+ const parentCtx = this.getClosestContext(rtBtn) || {};
1119
+ let payload = {};
1120
+ if (actionData) {
1121
+ let resolvedDataStr = actionData;
1122
+ for (const k in parentCtx) {
1123
+ const escapedK = escapeRegExp(k);
1124
+ resolvedDataStr = resolvedDataStr.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), parentCtx[k] !== void 0 && parentCtx[k] !== null ? parentCtx[k] : "");
1125
+ }
1126
+ try {
1127
+ payload = JSON.parse(resolvedDataStr);
1128
+ } catch {
1129
+ payload = {};
1130
+ }
1131
+ }
1132
+ this.publish(topic, payload);
1133
+ }
1134
+ if (apiBtn) {
1135
+ if (evtName === "submit") e.preventDefault();
1136
+ const apiTarget = apiBtn.getAttribute(`data-api-${evtName}`);
1137
+ const actionData = apiBtn.getAttribute("data-api-payload");
1138
+ const parentCtx = this.getClosestContext(apiBtn) || {};
1139
+ const parts = apiTarget.trim().split(" ");
1140
+ const method = parts.length > 1 ? parts[0].toUpperCase() : "POST";
1141
+ const path = parts.length > 1 ? parts[1] : parts[0];
1142
+ let payload = null;
1143
+ if (actionData) {
1144
+ let resolvedDataStr = actionData;
1145
+ for (const k in parentCtx) {
1146
+ const escapedK = escapeRegExp(k);
1147
+ resolvedDataStr = resolvedDataStr.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), parentCtx[k] !== void 0 && parentCtx[k] !== null ? parentCtx[k] : "");
1148
+ }
1149
+ try {
1150
+ payload = JSON.parse(resolvedDataStr);
1151
+ } catch {
1152
+ payload = null;
1153
+ }
1154
+ }
1155
+ try {
1156
+ const result = await this.api.request(method, path, payload);
1157
+ const resultBind = apiBtn.getAttribute("data-api-result");
1158
+ if (resultBind) this._updateDOM(resultBind, result);
1159
+ const redirect = apiBtn.getAttribute("data-api-redirect");
1160
+ if (redirect) window.location.href = redirect;
1161
+ if (apiBtn.hasAttribute("data-api-reload")) window.location.reload();
1162
+ } catch (err) {
1163
+ console.error(`[Dolphin] API ${evtName} Error:`, err);
1164
+ }
1165
+ }
1166
+ });
1167
+ });
1168
+ this.subscribe("#", (payload, topic) => {
1169
+ this._updateDOM(topic, payload);
1170
+ });
1171
+ this._scanAndFetchAPIBinds();
1172
+ this._scanStoreBinds();
1173
+ };
1174
+ clientProto._scanAndFetchAPIBinds = async function() {
1175
+ if (typeof document === "undefined") return;
1176
+ const elements = document.querySelectorAll("[data-api-get]");
1177
+ for (const el of Array.from(elements)) {
1178
+ const path = el.getAttribute("data-api-get");
1179
+ if (!path) continue;
1180
+ try {
1181
+ const result = await this.api.get(path);
1182
+ const rtBind = el.getAttribute("data-rt-bind");
1183
+ if (rtBind) {
1184
+ this._updateDOM(rtBind, result);
1185
+ } else {
1186
+ const template = resolveTemplate(el);
1187
+ if (template && typeof result === "object" && result !== null) {
1188
+ if (Array.isArray(result)) {
1189
+ let combinedHTML = "";
1190
+ for (const item of result) {
1191
+ let finalItemHTML = template;
1192
+ for (let key in item) {
1193
+ const escapedKey = escapeRegExp(key);
1194
+ finalItemHTML = finalItemHTML.replace(new RegExp(`\\{\\{${escapedKey}\\}\\}`, "g"), item[key] !== void 0 && item[key] !== null ? item[key] : "");
1195
+ }
1196
+ combinedHTML += finalItemHTML;
1197
+ }
1198
+ scheduleDOMUpdate(el, combinedHTML);
1199
+ } else {
1200
+ let finalHTML = template;
1201
+ for (let key in result) {
1202
+ const escapedKey = escapeRegExp(key);
1203
+ finalHTML = finalHTML.replace(new RegExp(`\\{\\{${escapedKey}\\}\\}`, "g"), result[key] !== void 0 && result[key] !== null ? result[key] : "");
1204
+ }
1205
+ scheduleDOMUpdate(el, finalHTML);
1206
+ }
1207
+ } else {
1208
+ if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
1209
+ el.value = typeof result === "object" ? result.value !== void 0 ? result.value : "" : result;
1210
+ } else {
1211
+ el.innerHTML = typeof result === "object" ? result.html || result.text || JSON.stringify(result) : result;
1212
+ }
1213
+ }
1214
+ }
1215
+ } catch (e) {
1216
+ console.error("[Dolphin] API Get Error:", e);
1217
+ }
1218
+ }
1219
+ };
1220
+ clientProto._updateDOM = function(topic, payload) {
1221
+ if (typeof document === "undefined") return;
1222
+ const elements = document.querySelectorAll(`[data-rt-bind="${topic}"]`);
1223
+ elements.forEach((el) => {
1224
+ if (el.getAttribute("data-rt-type") === "context" && typeof payload === "object" && payload !== null) {
1225
+ el._rtContext = payload;
1226
+ const processNode = (node) => {
1227
+ if (node.hasAttribute("data-rt-text")) {
1228
+ const key = node.getAttribute("data-rt-text");
1229
+ if (key && payload[key] !== void 0 && payload[key] !== null) node.textContent = payload[key];
1230
+ }
1231
+ if (node.hasAttribute("data-rt-html")) {
1232
+ const key = node.getAttribute("data-rt-html");
1233
+ if (key && payload[key] !== void 0 && payload[key] !== null) {
1234
+ node.innerHTML = sanitizeHTML(payload[key]);
1235
+ }
1236
+ }
1237
+ if (node.hasAttribute("data-rt-attr")) {
1238
+ const attrStr = node.getAttribute("data-rt-attr");
1239
+ if (attrStr) {
1240
+ attrStr.split(",").forEach((b) => {
1241
+ const parts = b.split(":");
1242
+ if (parts.length === 2) {
1243
+ const attrName = parts[0].trim();
1244
+ const key = parts[1].trim();
1245
+ if (attrName && key && payload[key] !== void 0 && payload[key] !== null) {
1246
+ node.setAttribute(attrName, payload[key]);
1247
+ }
1248
+ }
1249
+ });
1250
+ }
1251
+ }
1252
+ if (node.hasAttribute("data-rt-class")) {
1253
+ const classStr = node.getAttribute("data-rt-class");
1254
+ if (classStr) {
1255
+ classStr.split(",").forEach((b) => {
1256
+ const parts = b.split(":");
1257
+ if (parts.length === 2) {
1258
+ const className = parts[0].trim();
1259
+ const key = parts[1].trim();
1260
+ if (payload[key]) {
1261
+ node.classList.add(className);
1262
+ } else {
1263
+ node.classList.remove(className);
1264
+ }
1265
+ }
1266
+ });
1267
+ }
1268
+ }
1269
+ if (node.hasAttribute("data-rt-if")) {
1270
+ const key = node.getAttribute("data-rt-if");
1271
+ if (key) {
1272
+ if (payload[key]) {
1273
+ node.style.display = "";
1274
+ } else {
1275
+ node.style.display = "none";
1276
+ }
1277
+ }
1278
+ }
1279
+ if (node.hasAttribute("data-rt-hide")) {
1280
+ const key = node.getAttribute("data-rt-hide");
1281
+ if (key) {
1282
+ if (payload[key]) {
1283
+ node.style.display = "none";
1284
+ } else {
1285
+ node.style.display = "";
1286
+ }
1287
+ }
1288
+ }
1289
+ };
1290
+ processNode(el);
1291
+ el.querySelectorAll("[data-rt-text], [data-rt-html], [data-rt-attr], [data-rt-class], [data-rt-if], [data-rt-hide]").forEach(processNode);
1292
+ return;
1293
+ }
1294
+ const template = resolveTemplate(el);
1295
+ if (template && typeof payload === "object" && payload !== null) {
1296
+ if (Array.isArray(payload)) {
1297
+ let combinedHTML = "";
1298
+ for (const item of payload) {
1299
+ let finalItemHTML = template;
1300
+ for (let key in item) {
1301
+ const escapedKey = escapeRegExp(key);
1302
+ finalItemHTML = finalItemHTML.replace(new RegExp(`\\{\\{${escapedKey}\\}\\}`, "g"), item[key] !== void 0 && item[key] !== null ? item[key] : "");
1303
+ }
1304
+ combinedHTML += finalItemHTML;
1305
+ }
1306
+ scheduleDOMUpdate(el, combinedHTML);
1307
+ } else {
1308
+ let finalHTML = template;
1309
+ for (let key in payload) {
1310
+ const escapedKey = escapeRegExp(key);
1311
+ finalHTML = finalHTML.replace(new RegExp(`\\{\\{${escapedKey}\\}\\}`, "g"), payload[key] !== void 0 && payload[key] !== null ? payload[key] : "");
1312
+ }
1313
+ scheduleDOMUpdate(el, finalHTML);
1314
+ }
1315
+ return;
1316
+ }
1317
+ if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
1318
+ el.value = typeof payload === "object" ? payload.value !== void 0 ? payload.value : "" : payload;
1319
+ } else {
1320
+ el.innerHTML = typeof payload === "object" ? payload.html || payload.text || JSON.stringify(payload) : payload;
1321
+ }
1322
+ });
1323
+ };
1324
+ }
1325
+
1326
+ // src/offline.ts
1327
+ var DolphinOffline = class {
1328
+ client;
1329
+ db;
1330
+ isOnline;
1331
+ memoryCache = /* @__PURE__ */ new Map();
1332
+ memoryMutations = [];
1333
+ constructor(client) {
1334
+ this.client = client;
1335
+ this.isOnline = typeof window !== "undefined" && typeof navigator !== "undefined" ? navigator.onLine : true;
1336
+ this.initDB();
1337
+ this.setupNetworkListeners();
1338
+ }
1339
+ initDB() {
1340
+ if (typeof indexedDB === "undefined") return;
1341
+ try {
1342
+ const request = indexedDB.open("dolphin_offline", 1);
1343
+ request.onupgradeneeded = (e) => {
1344
+ const db = e.target.result;
1345
+ if (!db.objectStoreNames.contains("cache")) {
1346
+ db.createObjectStore("cache");
1347
+ }
1348
+ if (!db.objectStoreNames.contains("mutations")) {
1349
+ db.createObjectStore("mutations", { keyPath: "id", autoIncrement: true });
1350
+ }
1351
+ };
1352
+ request.onsuccess = (e) => {
1353
+ this.db = e.target.result;
1354
+ if (this.isOnline) {
1355
+ this.syncMutations();
1356
+ }
1357
+ };
1358
+ } catch (err) {
1359
+ console.warn("[Dolphin Offline] Failed to initialize IndexedDB:", err);
1360
+ }
1361
+ }
1362
+ setupNetworkListeners() {
1363
+ if (typeof window === "undefined") return;
1364
+ this.client.addDomListener(window, "online", () => {
1365
+ this.isOnline = true;
1366
+ this.client._dispatch("network:status", { online: true });
1367
+ this.syncMutations();
1368
+ });
1369
+ this.client.addDomListener(window, "offline", () => {
1370
+ this.isOnline = false;
1371
+ this.client._dispatch("network:status", { online: false });
1372
+ });
1373
+ }
1374
+ async getCache(key) {
1375
+ if (!this.db) {
1376
+ return this.memoryCache.get(key);
1377
+ }
1378
+ return new Promise((resolve) => {
1379
+ try {
1380
+ const transaction = this.db.transaction("cache", "readonly");
1381
+ const store = transaction.objectStore("cache");
1382
+ const req = store.get(key);
1383
+ req.onsuccess = () => resolve(req.result ? req.result.data : null);
1384
+ req.onerror = () => resolve(null);
1385
+ } catch {
1386
+ resolve(null);
1387
+ }
1388
+ });
1389
+ }
1390
+ async setCache(key, data) {
1391
+ if (!this.db) {
1392
+ this.memoryCache.set(key, data);
1393
+ return;
1394
+ }
1395
+ return new Promise((resolve) => {
1396
+ try {
1397
+ const transaction = this.db.transaction("cache", "readwrite");
1398
+ const store = transaction.objectStore("cache");
1399
+ store.put({ data, timestamp: Date.now() }, key);
1400
+ transaction.oncomplete = () => resolve();
1401
+ } catch {
1402
+ resolve();
1403
+ }
1404
+ });
1405
+ }
1406
+ async queueMutation(method, path, payload) {
1407
+ const mutation = {
1408
+ method,
1409
+ path,
1410
+ payload,
1411
+ timestamp: Date.now()
1412
+ };
1413
+ if (!this.db) {
1414
+ this.memoryMutations.push(mutation);
1415
+ return;
1416
+ }
1417
+ return new Promise((resolve) => {
1418
+ try {
1419
+ const transaction = this.db.transaction("mutations", "readwrite");
1420
+ const store = transaction.objectStore("mutations");
1421
+ store.add(mutation);
1422
+ transaction.oncomplete = () => resolve();
1423
+ } catch {
1424
+ resolve();
1425
+ }
1426
+ });
1427
+ }
1428
+ async getMutations() {
1429
+ if (!this.db) {
1430
+ return [...this.memoryMutations];
1431
+ }
1432
+ return new Promise((resolve) => {
1433
+ try {
1434
+ const transaction = this.db.transaction("mutations", "readonly");
1435
+ const store = transaction.objectStore("mutations");
1436
+ const req = store.getAll();
1437
+ req.onsuccess = () => resolve(req.result || []);
1438
+ req.onerror = () => resolve([]);
1439
+ } catch {
1440
+ resolve([]);
1441
+ }
1442
+ });
1443
+ }
1444
+ async removeMutation(id) {
1445
+ if (!this.db) {
1446
+ this.memoryMutations = this.memoryMutations.filter((m) => m.id !== id);
1447
+ return;
1448
+ }
1449
+ return new Promise((resolve) => {
1450
+ try {
1451
+ const transaction = this.db.transaction("mutations", "readwrite");
1452
+ const store = transaction.objectStore("mutations");
1453
+ store.delete(id);
1454
+ transaction.oncomplete = () => resolve();
1455
+ } catch {
1456
+ resolve();
1457
+ }
1458
+ });
1459
+ }
1460
+ async syncMutations() {
1461
+ const mutations = await this.getMutations();
1462
+ if (mutations.length === 0) return;
1463
+ console.log(`[Dolphin Offline] Syncing ${mutations.length} queued mutations...`);
1464
+ for (const mutation of mutations) {
1465
+ try {
1466
+ await this.client.api.requestDirect(mutation.method, mutation.path, mutation.payload);
1467
+ if (mutation.id !== void 0) {
1468
+ await this.removeMutation(mutation.id);
1469
+ } else {
1470
+ this.memoryMutations.shift();
1471
+ }
1472
+ } catch (err) {
1473
+ console.error(`[Dolphin Offline] Sync failed for mutation ${mutation.method} ${mutation.path}:`, err);
1474
+ if (err && err.status && err.status >= 400 && err.status < 500) {
1475
+ console.warn("[Dolphin Offline] Discarding invalid mutation.");
1476
+ if (mutation.id !== void 0) {
1477
+ await this.removeMutation(mutation.id);
1478
+ } else {
1479
+ this.memoryMutations.shift();
1480
+ }
1481
+ } else {
1482
+ break;
1483
+ }
1484
+ }
1485
+ }
1486
+ }
1487
+ };
1488
+ function attachOffline(clientProto) {
1489
+ clientProto._initOffline = function() {
1490
+ this.offline = new DolphinOffline(this);
1491
+ };
1492
+ }
1493
+
1494
+ // src/validation.ts
1495
+ function validateField(value, rulesStr, allValues) {
1496
+ const rules = rulesStr.split(",");
1497
+ for (const rule of rules) {
1498
+ const parts = rule.trim().split(":");
1499
+ const ruleName = parts[0];
1500
+ const ruleArg = parts[1];
1501
+ if (ruleName === "required") {
1502
+ if (!value || value.trim() === "") {
1503
+ return "This field is required";
1504
+ }
1505
+ } else if (ruleName === "email") {
1506
+ if (value && value.trim() !== "") {
1507
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1508
+ if (!emailRegex.test(value)) {
1509
+ return "Please enter a valid email address";
1510
+ }
1511
+ }
1512
+ } else if (ruleName === "min") {
1513
+ const minLen = parseInt(ruleArg, 10);
1514
+ if (!value || value.length < minLen) {
1515
+ return `Must be at least ${minLen} characters`;
1516
+ }
1517
+ } else if (ruleName === "match") {
1518
+ if (allValues && value !== allValues[ruleArg]) {
1519
+ return `Must match ${ruleArg}`;
1520
+ }
1521
+ }
1522
+ }
1523
+ return null;
1524
+ }
1525
+ function attachValidation(clientProto) {
1526
+ clientProto.validateField = validateField;
1527
+ }
1528
+
1529
+ // src/animation.ts
1530
+ function attachAnimations(clientProto) {
1531
+ clientProto.animateElement = function(el, animationClass, durationMs = 300) {
1532
+ if (typeof el.animate !== "function") {
1533
+ el.classList.add(animationClass);
1534
+ setTimeout(() => el.classList.remove(animationClass), durationMs);
1535
+ return;
1536
+ }
1537
+ if (animationClass === "fade-in") {
1538
+ el.animate([
1539
+ { opacity: 0, transform: "translateY(10px)" },
1540
+ { opacity: 1, transform: "translateY(0)" }
1541
+ ], { duration: durationMs, easing: "ease-out" });
1542
+ } else if (animationClass === "fade-out") {
1543
+ el.animate([
1544
+ { opacity: 1, transform: "translateY(0)" },
1545
+ { opacity: 0, transform: "translateY(10px)" }
1546
+ ], { duration: durationMs, easing: "ease-in" });
1547
+ }
1548
+ };
1549
+ clientProto.staggerListItems = function(container, itemSelector, delayMs = 50) {
1550
+ if (typeof document === "undefined") return;
1551
+ const items = container.querySelectorAll(itemSelector);
1552
+ items.forEach((item, idx) => {
1553
+ item.style.animationDelay = `${idx * delayMs}ms`;
1554
+ item.classList.add("staggered-item");
1555
+ });
1556
+ };
1557
+ }
1558
+
1559
+ // src/a11y.ts
1560
+ function attachA11y(clientProto) {
1561
+ clientProto._initA11y = function() {
1562
+ if (typeof document === "undefined") return;
1563
+ this.addDomListener(document, "keydown", (e) => {
1564
+ if (e.key !== "Tab") return;
1565
+ const trappedContainers = document.querySelectorAll("[data-rt-a11y-focus-trap]");
1566
+ trappedContainers.forEach((container) => {
1567
+ if (container.style.display === "none" || container.hasAttribute("aria-hidden") && container.getAttribute("aria-hidden") === "true") {
1568
+ return;
1569
+ }
1570
+ const focusableSelectors = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]';
1571
+ const focusableElements = Array.from(container.querySelectorAll(focusableSelectors));
1572
+ if (focusableElements.length === 0) return;
1573
+ const firstEl = focusableElements[0];
1574
+ const lastEl = focusableElements[focusableElements.length - 1];
1575
+ if (e.shiftKey) {
1576
+ if (document.activeElement === firstEl) {
1577
+ lastEl.focus();
1578
+ e.preventDefault();
1579
+ }
1580
+ } else {
1581
+ if (document.activeElement === lastEl) {
1582
+ firstEl.focus();
1583
+ e.preventDefault();
1584
+ }
1585
+ }
1586
+ });
1587
+ });
1588
+ this.addDomListener(document, "keydown", (e) => {
1589
+ if (!["ArrowUp", "ArrowDown", "Enter"].includes(e.key)) return;
1590
+ const keyNavLists = document.querySelectorAll("[data-rt-keynav]");
1591
+ keyNavLists.forEach((list) => {
1592
+ const items = Array.from(list.children);
1593
+ if (items.length === 0) return;
1594
+ let activeIdx = items.findIndex((el) => el.classList.contains("active") || document.activeElement === el);
1595
+ if (e.key === "ArrowDown") {
1596
+ activeIdx = (activeIdx + 1) % items.length;
1597
+ items[activeIdx].focus();
1598
+ items.forEach((item, idx) => {
1599
+ if (idx === activeIdx) item.classList.add("active");
1600
+ else item.classList.remove("active");
1601
+ });
1602
+ e.preventDefault();
1603
+ } else if (e.key === "ArrowUp") {
1604
+ activeIdx = (activeIdx - 1 + items.length) % items.length;
1605
+ items[activeIdx].focus();
1606
+ items.forEach((item, idx) => {
1607
+ if (idx === activeIdx) item.classList.add("active");
1608
+ else item.classList.remove("active");
1609
+ });
1610
+ e.preventDefault();
1611
+ } else if (e.key === "Enter" && activeIdx !== -1) {
1612
+ items[activeIdx].click();
1613
+ e.preventDefault();
1614
+ }
1615
+ });
1616
+ });
1617
+ };
1618
+ clientProto.autoAriaModal = function(modalEl, isOpen) {
1619
+ if (isOpen) {
1620
+ modalEl.setAttribute("role", "dialog");
1621
+ modalEl.setAttribute("aria-modal", "true");
1622
+ modalEl.setAttribute("aria-hidden", "false");
1623
+ modalEl.focus();
1624
+ } else {
1625
+ modalEl.setAttribute("aria-hidden", "true");
1626
+ }
1627
+ };
1628
+ }
1629
+
1630
+ // src/i18n.ts
1631
+ function attachI18n(clientProto) {
1632
+ clientProto._initI18n = function() {
1633
+ this.i18n = this.i18n || {
1634
+ locale: "en",
1635
+ dicts: {}
1636
+ };
1637
+ if (typeof document === "undefined") return;
1638
+ const dictEls = document.querySelectorAll("[data-i18n-dict]");
1639
+ dictEls.forEach((el) => {
1640
+ const locale = el.getAttribute("data-i18n-dict");
1641
+ if (locale) {
1642
+ try {
1643
+ const dictData = JSON.parse(el.textContent || "{}");
1644
+ this.i18n.dicts[locale] = {
1645
+ ...this.i18n.dicts[locale] || {},
1646
+ ...dictData
1647
+ };
1648
+ } catch (e) {
1649
+ console.warn("[Dolphin i18n] Failed to parse dictionary for locale:", locale, e);
1650
+ }
1651
+ }
1652
+ });
1653
+ if (!this.i18n.locale && typeof navigator !== "undefined") {
1654
+ const browserLang = navigator.language.split("-")[0];
1655
+ if (this.i18n.dicts[browserLang]) {
1656
+ this.i18n.locale = browserLang;
1657
+ }
1658
+ }
1659
+ this.addDomListener(document, "click", (e) => {
1660
+ const switcher = e.target.closest("[data-i18n-switch]");
1661
+ if (switcher) {
1662
+ const newLocale = switcher.getAttribute("data-i18n-switch");
1663
+ if (newLocale) {
1664
+ this.setLocale(newLocale);
1665
+ }
1666
+ }
1667
+ });
1668
+ this.translateDOM();
1669
+ };
1670
+ clientProto.setLocale = function(locale) {
1671
+ this.i18n = this.i18n || { locale: "en", dicts: {} };
1672
+ this.i18n.locale = locale;
1673
+ this.translateDOM();
1674
+ this.publish("i18n/locale", locale);
1675
+ };
1676
+ clientProto.translateDOM = function() {
1677
+ if (typeof document === "undefined") return;
1678
+ this.i18n = this.i18n || { locale: "en", dicts: {} };
1679
+ const currentLocale = this.i18n.locale || "en";
1680
+ const dict = this.i18n.dicts[currentLocale] || {};
1681
+ const translateEls = document.querySelectorAll("[data-i18n-key]");
1682
+ translateEls.forEach((el) => {
1683
+ const key = el.getAttribute("data-i18n-key");
1684
+ if (!key) return;
1685
+ let translation = key.split(".").reduce((o, i) => o ? o[i] : null, dict);
1686
+ if (translation === void 0 || translation === null) {
1687
+ translation = key;
1688
+ }
1689
+ const paramsAttr = el.getAttribute("data-i18n-params");
1690
+ if (paramsAttr) {
1691
+ try {
1692
+ const params = JSON.parse(paramsAttr);
1693
+ const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1694
+ for (const k in params) {
1695
+ const escapedK = escapeRegExp(k);
1696
+ translation = translation.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), params[k]);
1697
+ }
1698
+ } catch {
1699
+ }
1700
+ }
1701
+ if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
1702
+ el.placeholder = translation;
1703
+ } else {
1704
+ el.textContent = translation;
1705
+ }
1706
+ });
1707
+ };
1708
+ }
1709
+
1710
+ // src/dragdrop.ts
1711
+ function attachDragDrop(clientProto) {
1712
+ clientProto._initDragDrop = function() {
1713
+ if (typeof document === "undefined") return;
1714
+ this.addDomListener(document, "dragstart", (e) => {
1715
+ const dragEl = e.target.closest("[data-drag]");
1716
+ if (!dragEl) return;
1717
+ const payloadStr = dragEl.getAttribute("data-drag");
1718
+ if (payloadStr) {
1719
+ e.dataTransfer.setData("text/plain", payloadStr);
1720
+ e.dataTransfer.effectAllowed = "move";
1721
+ dragEl.classList.add("dragging");
1722
+ }
1723
+ });
1724
+ this.addDomListener(document, "dragend", (e) => {
1725
+ const dragEl = e.target.closest("[data-drag]");
1726
+ if (dragEl) {
1727
+ dragEl.classList.remove("dragging");
1728
+ }
1729
+ });
1730
+ this.addDomListener(document, "dragover", (e) => {
1731
+ const dropZone = e.target.closest("[data-drop]");
1732
+ if (dropZone) {
1733
+ e.preventDefault();
1734
+ dropZone.classList.add("drag-over");
1735
+ }
1736
+ });
1737
+ this.addDomListener(document, "dragleave", (e) => {
1738
+ const dropZone = e.target.closest("[data-drop]");
1739
+ if (dropZone) {
1740
+ dropZone.classList.remove("drag-over");
1741
+ }
1742
+ });
1743
+ this.addDomListener(document, "drop", (e) => {
1744
+ const dropZone = e.target.closest("[data-drop]");
1745
+ if (!dropZone) return;
1746
+ e.preventDefault();
1747
+ dropZone.classList.remove("drag-over");
1748
+ const topic = dropZone.getAttribute("data-drop");
1749
+ const dataStr = e.dataTransfer.getData("text/plain");
1750
+ if (topic && dataStr) {
1751
+ try {
1752
+ const payload = JSON.parse(dataStr);
1753
+ this.publish(topic, payload);
1754
+ } catch {
1755
+ this.publish(topic, { value: dataStr });
1756
+ }
1757
+ }
1758
+ });
1759
+ this.addDomListener(document, "dragover", (e) => {
1760
+ const sortableContainer = e.target.closest("[data-sortable]");
1761
+ if (!sortableContainer) return;
1762
+ e.preventDefault();
1763
+ const draggingEl = sortableContainer.querySelector(".dragging");
1764
+ if (!draggingEl) return;
1765
+ const siblings = Array.from(sortableContainer.querySelectorAll("[data-drag]:not(.dragging)"));
1766
+ const nextSibling = siblings.find((sibling) => {
1767
+ const box = sibling.getBoundingClientRect();
1768
+ const offset = e.clientY - box.top - box.height / 2;
1769
+ return offset < 0;
1770
+ });
1771
+ if (nextSibling) {
1772
+ sortableContainer.insertBefore(draggingEl, nextSibling);
1773
+ } else {
1774
+ sortableContainer.appendChild(draggingEl);
1775
+ }
1776
+ });
1777
+ this.addDomListener(document, "drop", (e) => {
1778
+ const sortableContainer = e.target.closest("[data-sortable]");
1779
+ if (!sortableContainer) return;
1780
+ const topic = sortableContainer.getAttribute("data-sortable");
1781
+ if (!topic) return;
1782
+ const elements = Array.from(sortableContainer.querySelectorAll("[data-drag]"));
1783
+ const newOrder = elements.map((el, index) => {
1784
+ const payloadStr = el.getAttribute("data-drag");
1785
+ try {
1786
+ return { index, payload: JSON.parse(payloadStr || "{}") };
1787
+ } catch {
1788
+ return { index, payload: payloadStr };
1789
+ }
1790
+ });
1791
+ this.publish(topic, newOrder);
1792
+ });
1793
+ };
1794
+ }
1795
+
1796
+ // src/collab.ts
1797
+ function attachCollab(clientProto) {
1798
+ clientProto._initCollab = function() {
1799
+ if (typeof document === "undefined") return;
1800
+ this.addDomListener(document, "mousemove", (e) => {
1801
+ const shareContainers = document.querySelectorAll("[data-rt-cursor-share]");
1802
+ shareContainers.forEach((container) => {
1803
+ const room = container.getAttribute("data-rt-cursor-share");
1804
+ if (!room) return;
1805
+ const box = container.getBoundingClientRect();
1806
+ const xRatio = (e.clientX - box.left) / box.width;
1807
+ const yRatio = (e.clientY - box.top) / box.height;
1808
+ const now = Date.now();
1809
+ if (!container._lastSent || now - container._lastSent > 50) {
1810
+ container._lastSent = now;
1811
+ this.pubPush(`collab/${room}/cursor/${this.deviceId}`, {
1812
+ deviceId: this.deviceId,
1813
+ x: xRatio,
1814
+ y: yRatio
1815
+ });
1816
+ }
1817
+ });
1818
+ });
1819
+ this.addDomListener(document, "input", (e) => {
1820
+ const typingBind = e.target.getAttribute("data-rt-typing");
1821
+ if (!typingBind) return;
1822
+ const room = typingBind;
1823
+ const publishTyping = (isTyping) => {
1824
+ this.pubPush(`collab/${room}/typing/${this.deviceId}`, {
1825
+ deviceId: this.deviceId,
1826
+ typing: isTyping
1827
+ });
1828
+ };
1829
+ if (!e.target._isTyping) {
1830
+ e.target._isTyping = true;
1831
+ publishTyping(true);
1832
+ }
1833
+ if (e.target._typingTimer) clearTimeout(e.target._typingTimer);
1834
+ e.target._typingTimer = setTimeout(() => {
1835
+ e.target._isTyping = false;
1836
+ publishTyping(false);
1837
+ }, 2e3);
1838
+ });
1839
+ this.addDomListener(document, "input", (e) => {
1840
+ const crdtBind = e.target.getAttribute("data-rt-crdt");
1841
+ if (!crdtBind) return;
1842
+ const docName = crdtBind;
1843
+ const value = e.target.value;
1844
+ const now = Date.now();
1845
+ this.publish(`collab/${docName}/crdt`, {
1846
+ deviceId: this.deviceId,
1847
+ value,
1848
+ timestamp: now,
1849
+ cursorPos: e.target.selectionStart
1850
+ });
1851
+ });
1852
+ this.subscribe("collab/+/cursor/+", (payload, topic) => {
1853
+ const parts = topic.split("/");
1854
+ const room = parts[1];
1855
+ const remoteDeviceId = parts[3];
1856
+ if (remoteDeviceId === this.deviceId) return;
1857
+ const container = document.querySelector(`[data-rt-cursor-share="${room}"]`);
1858
+ if (!container) return;
1859
+ let cursorEl = container.querySelector(`.rt-cursor-${remoteDeviceId}`);
1860
+ if (!cursorEl) {
1861
+ cursorEl = document.createElement("div");
1862
+ cursorEl.className = `rt-cursor rt-cursor-${remoteDeviceId}`;
1863
+ cursorEl.style.position = "absolute";
1864
+ cursorEl.style.width = "10px";
1865
+ cursorEl.style.height = "10px";
1866
+ cursorEl.style.borderRadius = "50%";
1867
+ cursorEl.style.backgroundColor = "#" + Math.floor(Math.random() * 16777215).toString(16);
1868
+ cursorEl.style.pointerEvents = "none";
1869
+ container.appendChild(cursorEl);
1870
+ }
1871
+ const box = container.getBoundingClientRect();
1872
+ cursorEl.style.left = payload.x * box.width + "px";
1873
+ cursorEl.style.top = payload.y * box.height + "px";
1874
+ });
1875
+ this.subscribe("collab/+/crdt", (payload, topic) => {
1876
+ if (payload.deviceId === this.deviceId) return;
1877
+ const parts = topic.split("/");
1878
+ const docName = parts[1];
1879
+ const crdtInputs = document.querySelectorAll(`[data-rt-crdt="${docName}"]`);
1880
+ crdtInputs.forEach((input) => {
1881
+ if (!input._lastUpdate || payload.timestamp > input._lastUpdate) {
1882
+ input._lastUpdate = payload.timestamp;
1883
+ const originalPos = input.selectionStart;
1884
+ input.value = payload.value;
1885
+ if (document.activeElement === input) {
1886
+ input.setSelectionRange(originalPos, originalPos);
1887
+ }
1888
+ }
1889
+ });
1890
+ });
1891
+ };
1892
+ }
1893
+
1894
+ // src/pwa.ts
1895
+ function attachPwa(clientProto) {
1896
+ clientProto.registerServiceWorker = async function(swPath = "/sw.js") {
1897
+ if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
1898
+ console.warn("[Dolphin PWA] Service Workers are not supported in this browser.");
1899
+ return null;
1900
+ }
1901
+ try {
1902
+ const registration = await navigator.serviceWorker.register(swPath);
1903
+ console.log("[Dolphin PWA] Service Worker registered successfully with scope:", registration.scope);
1904
+ return registration;
1905
+ } catch (e) {
1906
+ console.error("[Dolphin PWA] Service Worker registration failed:", e);
1907
+ return null;
1908
+ }
1909
+ };
1910
+ clientProto.subscribePushNotifications = async function(vapidPublicKey) {
1911
+ if (typeof window === "undefined" || !("serviceWorker" in navigator) || !("PushManager" in window)) {
1912
+ console.warn("[Dolphin PWA] Push notifications are not supported in this browser.");
1913
+ return null;
1914
+ }
1915
+ try {
1916
+ const registration = await navigator.serviceWorker.ready;
1917
+ let subscription = await registration.pushManager.getSubscription();
1918
+ if (!subscription) {
1919
+ const padding = "=".repeat((4 - vapidPublicKey.length % 4) % 4);
1920
+ const base64 = (vapidPublicKey + padding).replace(/\-/g, "+").replace(/_/g, "/");
1921
+ const rawData = window.atob(base64);
1922
+ const outputArray = new Uint8Array(rawData.length);
1923
+ for (let i = 0; i < rawData.length; ++i) {
1924
+ outputArray[i] = rawData.charCodeAt(i);
1925
+ }
1926
+ subscription = await registration.pushManager.subscribe({
1927
+ userVisibleOnly: true,
1928
+ applicationServerKey: outputArray
1929
+ });
1930
+ }
1931
+ console.log("[Dolphin PWA] Subscribed to push notifications:", subscription);
1932
+ return subscription;
1933
+ } catch (e) {
1934
+ console.error("[Dolphin PWA] Push notification subscription failed:", e);
1935
+ return null;
1936
+ }
1937
+ };
1938
+ }
1939
+
1940
+ // src/testing.ts
1941
+ var DolphinTestUtils = class {
1942
+ static render(html) {
1943
+ if (typeof document === "undefined") {
1944
+ throw new Error("DolphinTestUtils.render requires a DOM document environment to execute.");
1945
+ }
1946
+ const container = document.createElement("div");
1947
+ container.innerHTML = html;
1948
+ document.body.appendChild(container);
1949
+ return {
1950
+ container,
1951
+ find: (sel) => container.querySelector(sel),
1952
+ fireEvent: (el, eventType) => {
1953
+ const evt = document.createEvent("Event");
1954
+ evt.initEvent(eventType, true, true);
1955
+ el.dispatchEvent(evt);
1956
+ }
1957
+ };
1958
+ }
1959
+ static mockWebSocket() {
1960
+ const sentMessages = [];
1961
+ const mockWS = {
1962
+ readyState: 1,
1963
+ // OPEN
1964
+ send: (data) => {
1965
+ sentMessages.push(data);
1966
+ },
1967
+ close: jest.fn(),
1968
+ onopen: jest.fn(),
1969
+ onmessage: jest.fn(),
1970
+ onclose: jest.fn(),
1971
+ onerror: jest.fn(),
1972
+ sentMessages
1973
+ };
1974
+ global.WebSocket = class {
1975
+ static OPEN = 1;
1976
+ readyState = mockWS.readyState;
1977
+ send = mockWS.send;
1978
+ close = mockWS.close;
1979
+ set onopen(v) {
1980
+ mockWS.onopen = v;
1981
+ }
1982
+ get onopen() {
1983
+ return mockWS.onopen;
1984
+ }
1985
+ set onmessage(v) {
1986
+ mockWS.onmessage = v;
1987
+ }
1988
+ get onmessage() {
1989
+ return mockWS.onmessage;
1990
+ }
1991
+ set onclose(v) {
1992
+ mockWS.onclose = v;
1993
+ }
1994
+ get getonclose() {
1995
+ return mockWS.onclose;
1996
+ }
1997
+ constructor() {
1998
+ setTimeout(() => mockWS.onopen && mockWS.onopen(), 0);
1999
+ }
2000
+ };
2001
+ return mockWS;
2002
+ }
2003
+ static simulateClick(el) {
2004
+ const clickEvt = {
2005
+ target: el,
2006
+ preventDefault: jest.fn(),
2007
+ stopPropagation: jest.fn()
2008
+ };
2009
+ const clickListeners = global.document._listeners?.["click"] || [];
2010
+ clickListeners.forEach((listener) => listener(clickEvt));
2011
+ }
2012
+ static simulateChange(el, value) {
2013
+ el.value = value;
2014
+ const changeEvt = {
2015
+ target: el,
2016
+ preventDefault: jest.fn(),
2017
+ stopPropagation: jest.fn()
2018
+ };
2019
+ const changeListeners = global.document._listeners?.["change"] || [];
2020
+ changeListeners.forEach((listener) => listener(changeEvt));
2021
+ }
2022
+ };
2023
+ function attachTesting(clientProto) {
2024
+ clientProto.testing = DolphinTestUtils;
2025
+ }
2026
+
2027
+ // src/index.ts
2028
+ attachDOMBinding(DolphinClient.prototype);
2029
+ attachOffline(DolphinClient.prototype);
2030
+ attachValidation(DolphinClient.prototype);
2031
+ attachAnimations(DolphinClient.prototype);
2032
+ attachA11y(DolphinClient.prototype);
2033
+ attachI18n(DolphinClient.prototype);
2034
+ attachDragDrop(DolphinClient.prototype);
2035
+ attachCollab(DolphinClient.prototype);
2036
+ attachPwa(DolphinClient.prototype);
2037
+ attachTesting(DolphinClient.prototype);
2038
+ if (typeof window !== "undefined") {
2039
+ window.DolphinClient = DolphinClient;
2040
+ }
2041
+ return __toCommonJS(index_exports);
2042
+ })();