bitwrench 2.0.17 → 2.0.18

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 (67) hide show
  1. package/README.md +127 -38
  2. package/dist/bitwrench-bccl.cjs.js +8 -8
  3. package/dist/bitwrench-bccl.cjs.min.js +3 -3
  4. package/dist/bitwrench-bccl.esm.js +8 -8
  5. package/dist/bitwrench-bccl.esm.min.js +3 -3
  6. package/dist/bitwrench-bccl.umd.js +8 -8
  7. package/dist/bitwrench-bccl.umd.min.js +2 -2
  8. package/dist/bitwrench-code-edit.cjs.js +1 -1
  9. package/dist/bitwrench-code-edit.cjs.min.js +1 -1
  10. package/dist/bitwrench-code-edit.es5.js +1 -1
  11. package/dist/bitwrench-code-edit.es5.min.js +1 -1
  12. package/dist/bitwrench-code-edit.esm.js +1 -1
  13. package/dist/bitwrench-code-edit.esm.min.js +1 -1
  14. package/dist/bitwrench-code-edit.umd.js +1 -1
  15. package/dist/bitwrench-code-edit.umd.min.js +1 -1
  16. package/dist/bitwrench-lean.cjs.js +941 -775
  17. package/dist/bitwrench-lean.cjs.min.js +20 -20
  18. package/dist/bitwrench-lean.es5.js +1012 -961
  19. package/dist/bitwrench-lean.es5.min.js +18 -18
  20. package/dist/bitwrench-lean.esm.js +941 -775
  21. package/dist/bitwrench-lean.esm.min.js +20 -20
  22. package/dist/bitwrench-lean.umd.js +941 -775
  23. package/dist/bitwrench-lean.umd.min.js +20 -20
  24. package/dist/bitwrench-util-css.cjs.js +236 -0
  25. package/dist/bitwrench-util-css.cjs.min.js +22 -0
  26. package/dist/bitwrench-util-css.es5.js +414 -0
  27. package/dist/bitwrench-util-css.es5.min.js +21 -0
  28. package/dist/bitwrench-util-css.esm.js +230 -0
  29. package/dist/bitwrench-util-css.esm.min.js +21 -0
  30. package/dist/bitwrench-util-css.umd.js +242 -0
  31. package/dist/bitwrench-util-css.umd.min.js +21 -0
  32. package/dist/bitwrench.cjs.js +948 -782
  33. package/dist/bitwrench.cjs.min.js +21 -21
  34. package/dist/bitwrench.css +456 -132
  35. package/dist/bitwrench.es5.js +1024 -970
  36. package/dist/bitwrench.es5.min.js +19 -19
  37. package/dist/bitwrench.esm.js +949 -783
  38. package/dist/bitwrench.esm.min.js +21 -21
  39. package/dist/bitwrench.min.css +1 -1
  40. package/dist/bitwrench.umd.js +948 -782
  41. package/dist/bitwrench.umd.min.js +21 -21
  42. package/dist/builds.json +178 -90
  43. package/dist/bwserve.cjs.js +514 -68
  44. package/dist/bwserve.esm.js +513 -69
  45. package/dist/sri.json +44 -36
  46. package/package.json +3 -2
  47. package/readme.html +136 -49
  48. package/src/bitwrench-bccl.js +7 -7
  49. package/src/bitwrench-color-utils.js +31 -9
  50. package/src/bitwrench-esm-entry.js +11 -0
  51. package/src/bitwrench-styles.js +439 -232
  52. package/src/bitwrench-util-css.js +229 -0
  53. package/src/bitwrench.js +483 -485
  54. package/src/bwserve/attach.js +57 -0
  55. package/src/bwserve/bwclient.js +141 -0
  56. package/src/bwserve/bwshell.js +102 -0
  57. package/src/bwserve/client.js +151 -1
  58. package/src/bwserve/index.js +127 -28
  59. package/src/cli/attach.js +555 -0
  60. package/src/cli/convert.js +2 -5
  61. package/src/cli/index.js +7 -0
  62. package/src/cli/inject.js +1 -1
  63. package/src/cli/serve.js +6 -2
  64. package/src/generate-css.js +11 -4
  65. package/src/vendor/html2canvas.min.js +20 -0
  66. package/src/version.js +3 -3
  67. package/src/bwserve/shell.js +0 -106
@@ -0,0 +1,57 @@
1
+ /**
2
+ * bwserve attach — self-contained drop-in script generator.
3
+ *
4
+ * Generates JS that loads bitwrench + bwclient and auto-connects
5
+ * to a bwserve instance. When loaded in any browser page, it
6
+ * establishes an SSE connection for remote debugging.
7
+ *
8
+ * Usage:
9
+ * <script src="http://localhost:7902/bw/attach.js"></script>
10
+ *
11
+ * @module bwserve/attach
12
+ */
13
+
14
+ import { getBwClientSource } from './bwclient.js';
15
+ import { VERSION } from '../version.js';
16
+
17
+ /**
18
+ * Generate the self-contained attach script.
19
+ *
20
+ * The returned JS string, when evaluated in a browser:
21
+ * 1. Checks if bw is already loaded; if not, injects bitwrench UMD
22
+ * 2. Evaluates bwclient source to set up bw._bwClient
23
+ * 3. Calls bw._bwClient.attach() to connect via SSE
24
+ *
25
+ * @param {Object} [opts]
26
+ * @param {string} [opts.origin=''] - Server origin (empty = same origin)
27
+ * @returns {string} JavaScript source code
28
+ */
29
+ export function generateAttachScript(opts) {
30
+ opts = opts || {};
31
+ var origin = opts.origin || '';
32
+
33
+ var clientSource = getBwClientSource();
34
+
35
+ return '(function() {\n'
36
+ + ' "use strict";\n'
37
+ + ' var origin = ' + JSON.stringify(origin) + ';\n'
38
+ + ' function _go() {\n'
39
+ + ' ' + clientSource + '\n'
40
+ + ' bw._bwClient.attach(origin, {\n'
41
+ + ' allowExec: true,\n'
42
+ + ' onStatus: function(s) { console.log("[bw-attach] " + s); }\n'
43
+ + ' });\n'
44
+ + ' console.log("[bw-attach] v' + VERSION + ' connecting to " + (origin || location.origin));\n'
45
+ + ' }\n'
46
+ + ' if (window.bw) { _go(); return; }\n'
47
+ + ' var s = document.createElement("script");\n'
48
+ + ' s.src = (origin || "") + "/bw/lib/bitwrench.umd.js";\n'
49
+ + ' s.onload = function() {\n'
50
+ + ' if (typeof bw !== "undefined" && bw.loadStyles) bw.loadStyles();\n'
51
+ + ' _go();\n'
52
+ + ' };\n'
53
+ + ' document.head.appendChild(s);\n'
54
+ + '})();\n';
55
+ }
56
+
57
+ generateAttachScript.version = VERSION;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * bwclient.js — Browser-side protocol client for bwserve.
3
+ *
4
+ * Injected inline by bwshell. Requires window.bw (bitwrench loaded first).
5
+ * NOT bundled into bitwrench dist — this is a bwserve runtime asset.
6
+ *
7
+ * Responsibilities:
8
+ * - SSE connection lifecycle (connect, reconnect, status)
9
+ * - Unified POST-back via /bw/return/<route>/<clientId>
10
+ * - Register built-in client functions (scrollTo, focus, etc.)
11
+ * - data-bw-action click/key delegation
12
+ * - Attach mode for remote-controlling any bitwrench page
13
+ *
14
+ * @module bwserve/bwclient
15
+ */
16
+
17
+ import { VERSION } from '../version.js';
18
+
19
+ /**
20
+ * Return the bwclient source as a string for inline injection into the shell.
21
+ * The version is embedded at serve-time from package.json via version.js.
22
+ * @returns {string} JavaScript source code
23
+ */
24
+ export function getBwClientSource() {
25
+ return BWCLIENT_SOURCE.replace('__BW_VERSION__', VERSION);
26
+ }
27
+
28
+ var BWCLIENT_SOURCE = '(function(bw) {\n'
29
+ + ' "use strict";\n'
30
+ + ' if (!bw) return;\n'
31
+ + '\n'
32
+ + ' var _client = {\n'
33
+ + ' id: null,\n'
34
+ + ' version: "__BW_VERSION__",\n'
35
+ + ' status: "idle",\n'
36
+ + ' _es: null\n'
37
+ + ' };\n'
38
+ + '\n'
39
+ + ' // ── Unified POST-back ──\n'
40
+ + ' _client.respond = function(route, requestId, result, error) {\n'
41
+ + ' fetch("/bw/return/" + route + "/" + _client.id, {\n'
42
+ + ' method: "POST",\n'
43
+ + ' headers: { "Content-Type": "application/json" },\n'
44
+ + ' body: JSON.stringify({ requestId: requestId, route: route, result: result, error: error || null })\n'
45
+ + ' }).catch(function() {});\n'
46
+ + ' };\n'
47
+ + '\n'
48
+ + ' // ── SSE connect ──\n'
49
+ + ' _client.connect = function(url, opts) {\n'
50
+ + ' opts = opts || {};\n'
51
+ + ' var onStatus = opts.onStatus || function() {};\n'
52
+ + ' function setStatus(s) { _client.status = s; onStatus(s); }\n'
53
+ + ' setStatus("connecting");\n'
54
+ + ' if (typeof EventSource === "undefined") return;\n'
55
+ + ' var es = new EventSource(url);\n'
56
+ + ' _client._es = es;\n'
57
+ + ' es.onopen = function() { setStatus("connected"); };\n'
58
+ + ' es.onmessage = function(e) {\n'
59
+ + ' try {\n'
60
+ + ' var msg = typeof e.data === "string" ? bw.parseJSONFlex(e.data) : e.data;\n'
61
+ + ' bw.apply(msg);\n'
62
+ + ' } catch (err) {\n'
63
+ + ' if (typeof console !== "undefined") console.error("[bwclient]", err);\n'
64
+ + ' }\n'
65
+ + ' };\n'
66
+ + ' es.onerror = function() {\n'
67
+ + ' if (_client.status === "connected") setStatus("disconnected");\n'
68
+ + ' };\n'
69
+ + ' };\n'
70
+ + '\n'
71
+ + ' // ── Attach mode ──\n'
72
+ + ' _client.attach = function(url, opts) {\n'
73
+ + ' opts = opts || {};\n'
74
+ + ' _client.id = opts.clientId || "att_" + Math.random().toString(36).slice(2, 10);\n'
75
+ + ' if (opts.allowExec) bw._allowExec = true;\n'
76
+ + ' _client._registerBuiltins();\n'
77
+ + ' _client._wireActions();\n'
78
+ + ' _client.connect(url + "/bw/events/" + _client.id, opts);\n'
79
+ + ' };\n'
80
+ + '\n'
81
+ + ' // ── Send action to server ──\n'
82
+ + ' _client.sendAction = function(action, data) {\n'
83
+ + ' _client.respond("action", null, { action: action, data: data || {} });\n'
84
+ + ' };\n'
85
+ + '\n'
86
+ + ' // ── Register built-in functions ──\n'
87
+ + ' _client._registerBuiltins = function() {\n'
88
+ + ' var builtins = {\n'
89
+ + ' scrollTo: "function(sel){var el=bw._el(sel);if(el)el.scrollTop=el.scrollHeight;}",\n'
90
+ + ' focus: "function(sel){var el=bw._el(sel);if(el&&typeof el.focus===\\"function\\")el.focus();}",\n'
91
+ + ' download: "function(fn,c,m){if(typeof document===\\"undefined\\")return;var b=new Blob([c],{type:m||\\"text/plain\\"});var a=document.createElement(\\"a\\");a.href=URL.createObjectURL(b);a.download=fn;a.click();URL.revokeObjectURL(a.href);}",\n'
92
+ + ' clipboard: "function(t){if(typeof navigator!==\\"undefined\\"&&navigator.clipboard)navigator.clipboard.writeText(t);}",\n'
93
+ + ' redirect: "function(u){if(typeof window!==\\"undefined\\")window.location.href=u;}",\n'
94
+ + ' log: "function(){console.log.apply(console,arguments);}",\n'
95
+ + ' _bw_query: "function(opts){if(!bw._bwClient)return;try{var r=new Function(opts.code)();if(r&&typeof r.then===\\"function\\"){r.then(function(v){bw._bwClient.respond(\\"query\\",opts.requestId,v);}).catch(function(e){bw._bwClient.respond(\\"query\\",opts.requestId,null,e.message);});}else{bw._bwClient.respond(\\"query\\",opts.requestId,r);}}catch(e){bw._bwClient.respond(\\"query\\",opts.requestId,null,e.message);}}",\n'
96
+ + ' _bw_mount: "function(opts){if(!bw._bwClient)return;try{var taco;var f=opts.factory;var n=f.replace(/-([a-z])/g,function(_,c){return c.toUpperCase();});if(bw.BCCL&&bw.BCCL[n]){taco=bw.make(n,opts.props||{});}else if(bw._allowExec){taco=new Function(\\"props\\",f)(opts.props||{});}else{throw new Error(\\"Unknown component and allowExec disabled\\");}bw.DOM(opts.target,taco);bw._bwClient.respond(\\"mount\\",opts.requestId,{mounted:true});}catch(e){bw._bwClient.respond(\\"mount\\",opts.requestId,null,e.message);}}",\n'
97
+ + ' _bw_screenshot: "function(opts){if(!bw._bwClient)return;var sel=opts.selector||\\"body\\";var el=document.querySelector(sel);if(!el){bw._bwClient.respond(\\"screenshot\\",opts.requestId,null,\\"Element not found: \\"+sel);return;}function _ls(url){return new Promise(function(res,rej){var s=document.createElement(\\"script\\");s.src=url;s.onload=function(){res(window.html2canvas);};s.onerror=function(){rej(new Error(\\"Failed to load html2canvas\\"));};document.head.appendChild(s);});}var p=window.html2canvas?Promise.resolve(window.html2canvas):_ls(opts.captureUrl||\\"/bw/lib/vendor/html2canvas.min.js\\");p.then(function(h2c){return h2c(el,{scale:opts.scale||1,useCORS:true});}).then(function(canvas){var out=canvas;var mw=opts.maxWidth;var mh=opts.maxHeight;if((mw&&canvas.width>mw)||(mh&&canvas.height>mh)){var sw=mw?mw/canvas.width:1;var sh=mh?mh/canvas.height:1;var sc=Math.min(sw,sh);out=document.createElement(\\"canvas\\");out.width=Math.round(canvas.width*sc);out.height=Math.round(canvas.height*sc);out.getContext(\\"2d\\").drawImage(canvas,0,0,out.width,out.height);}var fmt=opts.format===\\"jpeg\\"?\\"image/jpeg\\":\\"image/png\\";var q=opts.format===\\"jpeg\\"?(opts.quality||0.85):undefined;var dataUrl=out.toDataURL(fmt,q);bw._bwClient.respond(\\"screenshot\\",opts.requestId,{data:dataUrl,width:out.width,height:out.height,format:opts.format||\\"png\\"});}).catch(function(err){bw._bwClient.respond(\\"screenshot\\",opts.requestId,null,err.message||String(err));});}",\n'
98
+ + ' _bw_tree: "function(opts){if(!bw._bwClient)return;var sel=opts.selector||\\"body\\";var depth=opts.depth||3;function walk(el,d){if(!el||d>depth)return null;var info={tag:el.tagName?el.tagName.toLowerCase():\\"#text\\"};if(el.id)info.id=el.id;if(el.className&&typeof el.className===\\"string\\")info.cls=el.className.split(\\" \\").slice(0,5).join(\\" \\");if(el.children&&el.children.length>0&&d<depth){info.children=[];for(var i=0;i<Math.min(el.children.length,20);i++){var c=walk(el.children[i],d+1);if(c)info.children.push(c);}}return info;}var root=document.querySelector(sel);bw._bwClient.respond(\\"query\\",opts.requestId,walk(root,0));}",\n'
99
+ + ' _bw_listen: "function(opts){if(!bw._bwClient)return;if(!bw._bwClient._listeners)bw._bwClient._listeners={};var key=opts.selector+\\":::\\"+opts.event;if(bw._bwClient._listeners[key])return;var fn=function(e){var el=e.target.closest?e.target.closest(opts.selector):null;if(!el)return;bw._bwClient.respond(\\"event\\",null,{event:opts.event,selector:opts.selector,tagName:el.tagName,id:el.id||null,text:(el.textContent||\\"\\").slice(0,100)});};document.addEventListener(opts.event,fn,true);bw._bwClient._listeners[key]={fn:fn,event:opts.event};}",\n'
100
+ + ' _bw_unlisten: "function(opts){if(!bw._bwClient||!bw._bwClient._listeners)return;var key=opts.selector+\\":::\\"+opts.event;var entry=bw._bwClient._listeners[key];if(!entry)return;document.removeEventListener(entry.event,entry.fn,true);delete bw._bwClient._listeners[key];}"\n'
101
+ + ' };\n'
102
+ + ' Object.keys(builtins).forEach(function(name) {\n'
103
+ + ' bw.apply({ type: "register", name: name, body: builtins[name] });\n'
104
+ + ' });\n'
105
+ + ' };\n'
106
+ + '\n'
107
+ + ' // ── Wire up data-bw-action click delegation ──\n'
108
+ + ' _client._wireActions = function() {\n'
109
+ + ' document.addEventListener("click", function(e) {\n'
110
+ + ' var el = e.target.closest ? e.target.closest("[data-bw-action]") : null;\n'
111
+ + ' if (!el) return;\n'
112
+ + ' e.preventDefault();\n'
113
+ + ' var actionData = {};\n'
114
+ + ' if (el.getAttribute("data-bw-id")) actionData.bwId = el.getAttribute("data-bw-id");\n'
115
+ + ' var form = el.closest("div") || document;\n'
116
+ + ' var inp = form.querySelector("input[type=text],input:not([type])");\n'
117
+ + ' if (inp) { actionData.inputValue = inp.value; inp.value = ""; }\n'
118
+ + ' _client.sendAction(el.getAttribute("data-bw-action"), actionData);\n'
119
+ + ' });\n'
120
+ + ' document.addEventListener("keydown", function(e) {\n'
121
+ + ' if (e.key === "Enter" && e.target.tagName === "INPUT") {\n'
122
+ + ' var form = e.target.closest("div") || document;\n'
123
+ + ' var btn = form.querySelector("[data-bw-action]");\n'
124
+ + ' if (btn) {\n'
125
+ + ' _client.sendAction(btn.getAttribute("data-bw-action"), { inputValue: e.target.value });\n'
126
+ + ' e.target.value = "";\n'
127
+ + ' }\n'
128
+ + ' }\n'
129
+ + ' });\n'
130
+ + ' };\n'
131
+ + '\n'
132
+ + ' // ── Event delegation helper ──\n'
133
+ + ' _client.listen = function(selector, event, action) {\n'
134
+ + ' document.addEventListener(event, function(e) {\n'
135
+ + ' var el = e.target.closest ? e.target.closest(selector) : null;\n'
136
+ + ' if (el) _client.sendAction(action, { selector: selector, event: event });\n'
137
+ + ' });\n'
138
+ + ' };\n'
139
+ + '\n'
140
+ + ' bw._bwClient = _client;\n'
141
+ + '})(window.bw);\n';
@@ -0,0 +1,102 @@
1
+ /**
2
+ * bwserve shell — generates the HTML page shell served to browsers.
3
+ *
4
+ * The shell is a minimal HTML doc that:
5
+ * - Loads bitwrench UMD + CSS from /bw/lib/ routes
6
+ * - Calls bw.loadStyles()
7
+ * - Optionally applies a custom theme
8
+ * - Creates a #app div
9
+ * - Inlines bwclient.js for SSE, action delegation, and built-ins
10
+ *
11
+ * @module bwserve/bwshell
12
+ */
13
+
14
+ import { getBwClientSource } from './bwclient.js';
15
+ import { VERSION } from '../version.js';
16
+
17
+ /**
18
+ * Generate the shell HTML page for a bwserve app.
19
+ *
20
+ * @param {Object} opts
21
+ * @param {string} opts.clientId - Unique client ID for this connection
22
+ * @param {string} [opts.title='bwserve'] - Page title
23
+ * @param {string} [opts.theme] - Theme preset name or config
24
+ * @param {boolean} [opts.injectBitwrench=true] - Whether to inject bitwrench scripts
25
+ * @param {boolean} [opts.allowExec=false] - Enable exec message type
26
+ * @returns {string} Complete HTML document
27
+ */
28
+ export function generateShell(opts) {
29
+ opts = opts || {};
30
+ var clientId = opts.clientId || 'default';
31
+ var title = opts.title || 'bwserve';
32
+ var inject = opts.injectBitwrench !== false;
33
+
34
+ var head = [
35
+ '<!DOCTYPE html>',
36
+ '<html lang="en">',
37
+ '<head>',
38
+ '<meta charset="UTF-8">',
39
+ '<meta name="viewport" content="width=device-width, initial-scale=1.0">',
40
+ '<title>' + title + '</title>',
41
+ '<meta name="generator" content="bwserve ' + VERSION + '">'
42
+ ];
43
+
44
+ if (inject) {
45
+ head.push('<script src="/bw/lib/bitwrench.umd.js"></script>');
46
+ head.push('<link rel="stylesheet" href="/bw/lib/bitwrench.css">');
47
+ }
48
+
49
+ head.push('</head>');
50
+ head.push('<body>');
51
+ head.push('<div id="app"></div>');
52
+
53
+ var script = [
54
+ '<script>',
55
+ '(function() {',
56
+ ' "use strict";',
57
+ ' bw.loadStyles();'
58
+ ];
59
+
60
+ if (opts.theme) {
61
+ script.push(' bw.loadStyles(' + JSON.stringify(
62
+ typeof opts.theme === 'string'
63
+ ? { primary: '#006666', secondary: '#333333' }
64
+ : opts.theme
65
+ ) + ');');
66
+ }
67
+
68
+ script.push('})();');
69
+ script.push('</script>');
70
+
71
+ // Inline bwclient.js
72
+ script.push('<script>');
73
+ script.push(getBwClientSource());
74
+ script.push('</script>');
75
+
76
+ // Init script: wire up bwclient
77
+ script.push('<script>');
78
+ script.push('(function() {');
79
+ script.push(' "use strict";');
80
+ script.push(' var clientId = ' + JSON.stringify(clientId) + ';');
81
+ if (opts.allowExec) {
82
+ script.push(' bw._allowExec = true;');
83
+ }
84
+ script.push(' bw._bwClient.id = clientId;');
85
+ script.push(' bw._bwClient._registerBuiltins();');
86
+ script.push(' bw._bwClient._wireActions();');
87
+ script.push(' bw._bwClient.connect("/bw/events/" + clientId, {');
88
+ script.push(' onStatus: function(s) {');
89
+ script.push(' if (typeof console !== "undefined") console.log("[bwserve] " + s);');
90
+ script.push(' }');
91
+ script.push(' });');
92
+ script.push('})();');
93
+ script.push('</script>');
94
+
95
+ script.push('</body>');
96
+ script.push('</html>');
97
+
98
+ return head.concat(script).join('\n');
99
+ }
100
+
101
+ /** bwshell version (from package.json) */
102
+ generateShell.version = VERSION;
@@ -17,15 +17,20 @@
17
17
  * @module bwserve/client
18
18
  */
19
19
 
20
+ import { VERSION } from '../version.js';
21
+
20
22
  /**
21
23
  * BwServeClient — one connected browser tab.
22
24
  */
23
25
  export class BwServeClient {
26
+ /** bwserve version (from package.json) */
27
+ static version = VERSION;
24
28
  constructor(id, res) {
25
29
  this.id = id;
26
30
  this._res = res; // SSE response stream (null in stub)
27
31
  this._handlers = {}; // action name → handler
28
32
  this._closed = false;
33
+ this._pending = {}; // requestId → { resolve, reject, timer }
29
34
  }
30
35
 
31
36
  /**
@@ -104,7 +109,7 @@ export class BwServeClient {
104
109
  /**
105
110
  * Call a previously registered or built-in function on the client.
106
111
  *
107
- * Built-in functions (always available, no registration needed):
112
+ * Built-in functions (registered by bwclient on connection):
108
113
  * scrollTo, focus, download, clipboard, redirect, log
109
114
  *
110
115
  * @param {string} name - Function name (registered or built-in)
@@ -167,6 +172,151 @@ export class BwServeClient {
167
172
  }
168
173
  }
169
174
 
175
+ // ── Pending promise mechanism ──
176
+
177
+ /**
178
+ * Create a pending promise with a unique requestId and timeout.
179
+ *
180
+ * @param {number} [timeout=10000] - Timeout in ms
181
+ * @returns {{ requestId: string, promise: Promise }}
182
+ * @private
183
+ */
184
+ _pend(timeout) {
185
+ var self = this;
186
+ timeout = timeout || 10000;
187
+ var requestId = 'req_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
188
+
189
+ var promise = new Promise(function(resolve, reject) {
190
+ var timer = setTimeout(function() {
191
+ delete self._pending[requestId];
192
+ reject(new Error('Request timeout after ' + timeout + 'ms'));
193
+ }, timeout);
194
+
195
+ self._pending[requestId] = { resolve: resolve, reject: reject, timer: timer };
196
+ });
197
+
198
+ return { requestId: requestId, promise: promise };
199
+ }
200
+
201
+ /**
202
+ * Resolve a pending promise by requestId.
203
+ * Called by the server route handler when a POST-back arrives.
204
+ *
205
+ * @param {string} requestId
206
+ * @param {Object} data - Response data (may contain .error)
207
+ * @returns {boolean} true if a pending request was found and resolved
208
+ * @private
209
+ */
210
+ _resolvePending(requestId, data) {
211
+ var pending = this._pending[requestId];
212
+ if (!pending) return false;
213
+
214
+ clearTimeout(pending.timer);
215
+ delete this._pending[requestId];
216
+
217
+ if (data.error) {
218
+ pending.reject(new Error(data.error));
219
+ } else {
220
+ pending.resolve(data.result !== undefined ? data.result : data);
221
+ }
222
+ return true;
223
+ }
224
+
225
+ // ── Query ──
226
+
227
+ /**
228
+ * Execute code on the client and get the result back.
229
+ *
230
+ * @param {string} code - JavaScript code to evaluate (return value is sent back)
231
+ * @param {Object} [options]
232
+ * @param {number} [options.timeout=5000] - Timeout in ms
233
+ * @returns {Promise<*>} The result of evaluating the code
234
+ */
235
+ query(code, options) {
236
+ var opts = options || {};
237
+ var pend = this._pend(opts.timeout || 5000);
238
+ this.call('_bw_query', { code: code, requestId: pend.requestId });
239
+ return pend.promise;
240
+ }
241
+
242
+ // ── Mount ──
243
+
244
+ /**
245
+ * Mount a BCCL component or factory function on the client.
246
+ *
247
+ * @param {string} selector - CSS selector of target element
248
+ * @param {string} factory - BCCL component name (e.g. 'accordion') or JS factory code
249
+ * @param {Object} [props] - Props to pass to the component/factory
250
+ * @param {Object} [options]
251
+ * @param {number} [options.timeout=10000] - Timeout in ms
252
+ * @returns {Promise<Object>} Resolves with { mounted: true } on success
253
+ */
254
+ mount(selector, factory, props, options) {
255
+ var opts = options || {};
256
+ var pend = this._pend(opts.timeout || 10000);
257
+ this.call('_bw_mount', {
258
+ target: selector,
259
+ factory: factory,
260
+ props: props || {},
261
+ requestId: pend.requestId
262
+ });
263
+ return pend.promise;
264
+ }
265
+
266
+ // ── Screenshot ──
267
+
268
+ /**
269
+ * Capture a screenshot of the client's page or a specific element.
270
+ *
271
+ * Requires the server to be created with `{ allowScreenshot: true }`.
272
+ * Uses html2canvas on the client side (lazy-loaded on first call).
273
+ *
274
+ * @param {string} [selector='body'] - CSS selector of element to capture
275
+ * @param {Object} [options]
276
+ * @param {string} [options.format='png'] - 'png' or 'jpeg'
277
+ * @param {number} [options.quality=0.85] - JPEG quality 0-1 (ignored for PNG)
278
+ * @param {number} [options.maxWidth] - Resize if wider (preserves aspect ratio)
279
+ * @param {number} [options.maxHeight] - Resize if taller (preserves aspect ratio)
280
+ * @param {number} [options.scale=1] - Device pixel ratio override
281
+ * @param {number} [options.timeout=10000] - Reject after ms
282
+ * @returns {Promise<Object>} { data: Buffer, width, height, format }
283
+ */
284
+ screenshot(selector, options) {
285
+ var self = this;
286
+ var opts = options || {};
287
+ var timeout = opts.timeout || 10000;
288
+
289
+ if (!self._allowScreenshot) {
290
+ return Promise.reject(new Error('Screenshot not enabled. Set allowScreenshot: true in server options.'));
291
+ }
292
+
293
+ var pend = self._pend(timeout);
294
+
295
+ // Call the bwclient-registered capture function
296
+ self.call('_bw_screenshot', {
297
+ requestId: pend.requestId,
298
+ selector: selector || 'body',
299
+ format: opts.format || 'png',
300
+ quality: opts.quality || 0.85,
301
+ maxWidth: opts.maxWidth || null,
302
+ maxHeight: opts.maxHeight || null,
303
+ scale: opts.scale || 1,
304
+ captureUrl: '/bw/lib/vendor/html2canvas.min.js'
305
+ });
306
+
307
+ // Transform the raw response into { data: Buffer, width, height, format }
308
+ return pend.promise.then(function(result) {
309
+ if (!result || !result.data) return result;
310
+ var base64 = result.data.split(',')[1];
311
+ return {
312
+ data: Buffer.from(base64, 'base64'),
313
+ width: result.width,
314
+ height: result.height,
315
+ format: result.format
316
+ };
317
+ });
318
+ }
319
+
170
320
  /**
171
321
  * Dispatch an incoming action from the client.
172
322
  * @private