bitwrench 2.0.31 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +210 -125
  2. package/dist/bitwrench-bccl.cjs.js +349 -188
  3. package/dist/bitwrench-bccl.cjs.min.js +2 -39
  4. package/dist/bitwrench-bccl.cjs.min.js.gz +0 -0
  5. package/dist/bitwrench-bccl.esm.js +349 -188
  6. package/dist/bitwrench-bccl.esm.min.js +2 -39
  7. package/dist/bitwrench-bccl.esm.min.js.gz +0 -0
  8. package/dist/bitwrench-bccl.umd.js +349 -188
  9. package/dist/bitwrench-bccl.umd.min.js +2 -39
  10. package/dist/bitwrench-bccl.umd.min.js.gz +0 -0
  11. package/dist/bitwrench-code-edit.cjs.js +17 -6
  12. package/dist/bitwrench-code-edit.cjs.min.js +2 -20
  13. package/dist/bitwrench-code-edit.es5.js +8 -3
  14. package/dist/bitwrench-code-edit.es5.min.js +2 -19
  15. package/dist/bitwrench-code-edit.esm.js +17 -6
  16. package/dist/bitwrench-code-edit.esm.min.js +2 -19
  17. package/dist/bitwrench-code-edit.umd.js +17 -6
  18. package/dist/bitwrench-code-edit.umd.min.js +2 -19
  19. package/dist/bitwrench-code-edit.umd.min.js.gz +0 -0
  20. package/dist/bitwrench-debug.js +1 -1
  21. package/dist/bitwrench-debug.min.js +1 -1
  22. package/dist/bitwrench-lean.cjs.js +2492 -1628
  23. package/dist/bitwrench-lean.cjs.min.js +2 -80
  24. package/dist/bitwrench-lean.cjs.min.js.gz +0 -0
  25. package/dist/bitwrench-lean.es5.js +2740 -1838
  26. package/dist/bitwrench-lean.es5.min.js +2 -49
  27. package/dist/bitwrench-lean.es5.min.js.gz +0 -0
  28. package/dist/bitwrench-lean.esm.js +2492 -1628
  29. package/dist/bitwrench-lean.esm.min.js +2 -80
  30. package/dist/bitwrench-lean.esm.min.js.gz +0 -0
  31. package/dist/bitwrench-lean.umd.js +2492 -1628
  32. package/dist/bitwrench-lean.umd.min.js +2 -80
  33. package/dist/bitwrench-lean.umd.min.js.gz +0 -0
  34. package/dist/bitwrench-util-color.cjs.js +251 -0
  35. package/dist/bitwrench-util-color.cjs.min.js +3 -0
  36. package/dist/bitwrench-util-color.es5.js +256 -0
  37. package/dist/bitwrench-util-color.es5.min.js +3 -0
  38. package/dist/bitwrench-util-color.esm.js +241 -0
  39. package/dist/bitwrench-util-color.esm.min.js +3 -0
  40. package/dist/bitwrench-util-color.umd.js +257 -0
  41. package/dist/bitwrench-util-color.umd.min.js +3 -0
  42. package/dist/bitwrench-util-css.cjs.js +2 -1
  43. package/dist/bitwrench-util-css.cjs.min.js +2 -21
  44. package/dist/bitwrench-util-css.es5.js +2 -1
  45. package/dist/bitwrench-util-css.es5.min.js +2 -20
  46. package/dist/bitwrench-util-css.esm.js +2 -1
  47. package/dist/bitwrench-util-css.esm.min.js +1 -19
  48. package/dist/bitwrench-util-css.umd.js +2 -1
  49. package/dist/bitwrench-util-css.umd.min.js +2 -20
  50. package/dist/bitwrench-util-css.umd.min.js.gz +0 -0
  51. package/dist/bitwrench.cjs.js +2826 -1801
  52. package/dist/bitwrench.cjs.min.js +2 -99
  53. package/dist/bitwrench.cjs.min.js.gz +0 -0
  54. package/dist/bitwrench.css +403 -479
  55. package/dist/bitwrench.d.ts +70 -73
  56. package/dist/bitwrench.es5.js +3106 -2020
  57. package/dist/bitwrench.es5.min.js +2 -67
  58. package/dist/bitwrench.es5.min.js.gz +0 -0
  59. package/dist/bitwrench.esm.js +2826 -1801
  60. package/dist/bitwrench.esm.min.js +2 -99
  61. package/dist/bitwrench.esm.min.js.gz +0 -0
  62. package/dist/bitwrench.min.css +1 -1
  63. package/dist/bitwrench.umd.js +2826 -1801
  64. package/dist/bitwrench.umd.min.js +2 -99
  65. package/dist/bitwrench.umd.min.js.gz +0 -0
  66. package/dist/builds.json +222 -134
  67. package/dist/bwserve.cjs.js +308 -298
  68. package/dist/bwserve.d.ts +157 -0
  69. package/dist/bwserve.esm.js +309 -299
  70. package/dist/sri.json +54 -46
  71. package/docs/README.md +6 -3
  72. package/docs/app-patterns.md +7 -6
  73. package/docs/bitwrench-for-wasm.md +53 -54
  74. package/docs/bitwrench-mcp.md +2 -2
  75. package/docs/bitwrench-northstar-principles.md +406 -0
  76. package/docs/bitwrench-taco-schema-discussion.md +2 -2
  77. package/docs/bitwrench_api.md +191 -106
  78. package/docs/bitwrench_typescript_usage.md +5 -5
  79. package/docs/bw-attach.md +29 -75
  80. package/docs/bwserve.md +200 -168
  81. package/docs/cli.md +36 -12
  82. package/docs/component-cheatsheet.md +2 -2
  83. package/docs/component-library.md +4 -4
  84. package/docs/component-lifecycle.md +234 -0
  85. package/docs/drift-lint.md +268 -0
  86. package/docs/framework-translation-table.md +4 -4
  87. package/docs/llm-bitwrench-guide.md +60 -50
  88. package/docs/routing.md +11 -13
  89. package/docs/state-management.md +110 -109
  90. package/docs/taco-format.md +13 -14
  91. package/docs/theming.md +13 -3
  92. package/docs/thinking-in-bitwrench.md +858 -983
  93. package/docs/tutorial-bwserve.md +37 -36
  94. package/docs/tutorial-embedded.md +10 -21
  95. package/docs/tutorial-website.md +2 -2
  96. package/package.json +38 -7
  97. package/readme.html +262 -160
  98. package/src/bitwrench-bccl-entry.js +2 -2
  99. package/src/bitwrench-bccl.js +346 -185
  100. package/src/bitwrench-code-edit.js +16 -5
  101. package/src/bitwrench-color-utils.js +117 -181
  102. package/src/bitwrench-file-ops.js +2 -2
  103. package/src/bitwrench-lean.js +4 -3
  104. package/src/bitwrench-router.js +5 -2
  105. package/src/bitwrench-styles.js +420 -504
  106. package/src/bitwrench-util-color.js +240 -0
  107. package/src/bitwrench-util-css.js +1 -0
  108. package/src/bitwrench-utils.js +4 -0
  109. package/src/bitwrench.d.ts +70 -73
  110. package/src/bitwrench.h +5 -0
  111. package/src/bitwrench.js +1939 -933
  112. package/src/bwserve/attach.js +0 -1
  113. package/src/bwserve/bwclient.js +172 -32
  114. package/src/bwserve/bwshell.js +0 -4
  115. package/src/bwserve/client.js +59 -220
  116. package/src/bwserve/index.js +78 -42
  117. package/src/bwserve.d.ts +157 -0
  118. package/src/bwserve.h +5 -0
  119. package/src/cli/attach.js +12 -75
  120. package/src/cli/convert.js +2 -2
  121. package/src/cli/serve.js +37 -35
  122. package/src/generate-css.js +1 -1
  123. package/src/mcp/knowledge.js +4 -4
  124. package/src/mcp/live.js +21 -13
  125. package/src/mcp/tools.js +0 -1
  126. package/src/version.js +3 -7
@@ -38,7 +38,6 @@ export function generateAttachScript(opts) {
38
38
  + ' function _go() {\n'
39
39
  + ' ' + clientSource + '\n'
40
40
  + ' bw._bwClient.attach(origin, {\n'
41
- + ' allowExec: true,\n'
42
41
  + ' onStatus: function(s) { console.log("[bw-attach] " + s); }\n'
43
42
  + ' });\n'
44
43
  + ' console.log("[bw-attach] v' + VERSION + ' connecting to " + (origin || location.origin));\n'
@@ -8,7 +8,7 @@
8
8
  * - SSE connection lifecycle (connect, reconnect, status)
9
9
  * - Unified POST-back via /bw/return/<route>/<clientId>
10
10
  * - Register built-in client functions (scrollTo, focus, etc.)
11
- * - data-bw-action click/key delegation
11
+ * - bw_act_* class click/key delegation
12
12
  * - Attach mode for remote-controlling any bitwrench page
13
13
  *
14
14
  * @module bwserve/bwclient
@@ -38,11 +38,12 @@ var BWCLIENT_SOURCE = '(function(bw) {\n'
38
38
  + '\n'
39
39
  + ' // ── Unified POST-back ──\n'
40
40
  + ' _client.respond = function(route, requestId, result, error) {\n'
41
- + ' fetch("/bw/return/" + route + "/" + _client.id, {\n'
41
+ + ' var base = (typeof origin !== "undefined" ? origin : "");\n'
42
+ + ' fetch(base + "/bw/return/" + route + "/" + _client.id, {\n'
42
43
  + ' method: "POST",\n'
43
44
  + ' headers: { "Content-Type": "application/json" },\n'
44
45
  + ' body: JSON.stringify({ requestId: requestId, route: route, result: result, error: error || null })\n'
45
- + ' }).catch(function() {});\n'
46
+ + ' }).catch(function(e) { console.warn("[bwclient] respond failed:", e); });\n'
46
47
  + ' };\n'
47
48
  + '\n'
48
49
  + ' // ── SSE connect ──\n'
@@ -72,7 +73,6 @@ var BWCLIENT_SOURCE = '(function(bw) {\n'
72
73
  + ' _client.attach = function(url, opts) {\n'
73
74
  + ' opts = opts || {};\n'
74
75
  + ' _client.id = opts.clientId || "att_" + Math.random().toString(36).slice(2, 10);\n'
75
- + ' if (opts.allowExec) bw._allowExec = true;\n'
76
76
  + ' _client._registerBuiltins();\n'
77
77
  + ' _client._wireActions();\n'
78
78
  + ' _client.connect(url + "/bw/events/" + _client.id, opts);\n'
@@ -83,52 +83,192 @@ var BWCLIENT_SOURCE = '(function(bw) {\n'
83
83
  + ' _client.respond("action", null, { action: action, data: data || {} });\n'
84
84
  + ' };\n'
85
85
  + '\n'
86
- + ' // ── Register built-in functions ──\n'
86
+ + ' // ── Register built-in functions (safe closures, no string eval) ──\n'
87
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;var root=document.querySelector(sel);if(typeof bw.inspect===\\"function\\"&&bw.inspect.length===2){bw._bwClient.respond(\\"query\\",opts.requestId,bw.inspect(root,depth));return;}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;}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'
88
+ + ' bw.registerRemote("scrollTo", function(sel) {\n'
89
+ + ' var el = bw.el(sel); if (el) el.scrollTop = el.scrollHeight;\n'
90
+ + ' });\n'
91
+ + ' bw.registerRemote("focus", function(sel) {\n'
92
+ + ' var el = bw.el(sel); if (el && typeof el.focus === "function") el.focus();\n'
93
+ + ' });\n'
94
+ + ' bw.registerRemote("download", function(fn, c, m) {\n'
95
+ + ' if (typeof document === "undefined") return;\n'
96
+ + ' var b = new Blob([c], { type: m || "text/plain" });\n'
97
+ + ' var a = document.createElement("a");\n'
98
+ + ' a.href = URL.createObjectURL(b); a.download = fn; a.click();\n'
99
+ + ' URL.revokeObjectURL(a.href);\n'
100
+ + ' });\n'
101
+ + ' bw.registerRemote("clipboard", function(t) {\n'
102
+ + ' if (typeof navigator !== "undefined" && navigator.clipboard) navigator.clipboard.writeText(t);\n'
103
+ + ' });\n'
104
+ + ' bw.registerRemote("redirect", function(u) {\n'
105
+ + ' if (typeof window !== "undefined") window.location.href = u;\n'
106
+ + ' });\n'
107
+ + ' bw.registerRemote("log", function() {\n'
108
+ + ' console.log.apply(console, arguments);\n'
109
+ + ' });\n'
110
+ + ' bw.registerRemote("_bw_mount", function(opts) {\n'
111
+ + ' if (!bw._bwClient) return;\n'
112
+ + ' try {\n'
113
+ + ' var f = opts.factory;\n'
114
+ + ' var n = f.replace(/-([a-z])/g, function(_, c) { return c.toUpperCase(); });\n'
115
+ + ' if (bw.BCCL && bw.BCCL[n]) {\n'
116
+ + ' var taco = bw.make(n, opts.props || {});\n'
117
+ + ' bw.mount(opts.target, taco);\n'
118
+ + ' bw._bwClient.respond("mount", opts.requestId, { mounted: true });\n'
119
+ + ' } else {\n'
120
+ + ' throw new Error("Unknown BCCL component: " + f);\n'
121
+ + ' }\n'
122
+ + ' } catch (e) {\n'
123
+ + ' bw._bwClient.respond("mount", opts.requestId, null, e.message);\n'
124
+ + ' }\n'
125
+ + ' });\n'
126
+ + ' bw.registerRemote("_bw_screenshot", function(opts) {\n'
127
+ + ' if (!bw._bwClient) return;\n'
128
+ + ' var sel = opts.selector || "body";\n'
129
+ + ' var el = document.querySelector(sel);\n'
130
+ + ' if (!el) { bw._bwClient.respond("screenshot", opts.requestId, null, "Element not found: " + sel); return; }\n'
131
+ + ' function _ls(url) {\n'
132
+ + ' return new Promise(function(res, rej) {\n'
133
+ + ' var s = document.createElement("script"); s.src = url;\n'
134
+ + ' s.onload = function() { res(window.html2canvas); };\n'
135
+ + ' s.onerror = function() { rej(new Error("Failed to load html2canvas")); };\n'
136
+ + ' document.head.appendChild(s);\n'
137
+ + ' });\n'
138
+ + ' }\n'
139
+ + ' var p = window.html2canvas ? Promise.resolve(window.html2canvas) : _ls(opts.captureUrl || "/bw/lib/vendor/html2canvas.min.js");\n'
140
+ + ' p.then(function(h2c) { return h2c(el, { scale: opts.scale || 1, useCORS: true }); })\n'
141
+ + ' .then(function(canvas) {\n'
142
+ + ' var out = canvas;\n'
143
+ + ' var mw = opts.maxWidth; var mh = opts.maxHeight;\n'
144
+ + ' if ((mw && canvas.width > mw) || (mh && canvas.height > mh)) {\n'
145
+ + ' var sw = mw ? mw / canvas.width : 1; var sh = mh ? mh / canvas.height : 1;\n'
146
+ + ' var sc = Math.min(sw, sh);\n'
147
+ + ' out = document.createElement("canvas");\n'
148
+ + ' out.width = Math.round(canvas.width * sc); out.height = Math.round(canvas.height * sc);\n'
149
+ + ' out.getContext("2d").drawImage(canvas, 0, 0, out.width, out.height);\n'
150
+ + ' }\n'
151
+ + ' var fmt = opts.format === "jpeg" ? "image/jpeg" : "image/png";\n'
152
+ + ' var q = opts.format === "jpeg" ? (opts.quality || 0.85) : undefined;\n'
153
+ + ' var dataUrl = out.toDataURL(fmt, q);\n'
154
+ + ' bw._bwClient.respond("screenshot", opts.requestId, { data: dataUrl, width: out.width, height: out.height, format: opts.format || "png" });\n'
155
+ + ' }).catch(function(err) {\n'
156
+ + ' bw._bwClient.respond("screenshot", opts.requestId, null, err.message || String(err));\n'
157
+ + ' });\n'
158
+ + ' });\n'
159
+ + ' bw.registerRemote("_bw_tree", function(opts) {\n'
160
+ + ' if (!bw._bwClient) return;\n'
161
+ + ' var sel = opts.selector || "body"; var depth = opts.depth || 3;\n'
162
+ + ' var root = document.querySelector(sel);\n'
163
+ + ' if (typeof bw.inspect === "function" && bw.inspect.length === 2) {\n'
164
+ + ' bw._bwClient.respond("query", opts.requestId, bw.inspect(root, depth)); return;\n'
165
+ + ' }\n'
166
+ + ' function walk(el, d) {\n'
167
+ + ' if (!el || d > depth) return null;\n'
168
+ + ' var info = { tag: el.tagName ? el.tagName.toLowerCase() : "#text" };\n'
169
+ + ' if (el.id) info.id = el.id;\n'
170
+ + ' if (el.className && typeof el.className === "string") info.cls = el.className.split(" ").slice(0, 5).join(" ");\n'
171
+ + ' if (el.children && el.children.length > 0 && d < depth) {\n'
172
+ + ' info.children = [];\n'
173
+ + ' for (var i = 0; i < Math.min(el.children.length, 20); i++) {\n'
174
+ + ' var c = walk(el.children[i], d + 1); if (c) info.children.push(c);\n'
175
+ + ' }\n'
176
+ + ' }\n'
177
+ + ' return info;\n'
178
+ + ' }\n'
179
+ + ' bw._bwClient.respond("query", opts.requestId, walk(root, 0));\n'
180
+ + ' });\n'
181
+ + ' bw.registerRemote("_bw_query", function(opts) {\n'
182
+ + ' if (!bw._bwClient) return;\n'
183
+ + ' try {\n'
184
+ + ' var result;\n'
185
+ + ' try { result = new Function("return (" + opts.code + ")")(); }\n'
186
+ + ' catch (se) { result = new Function(opts.code)(); }\n'
187
+ + ' bw._bwClient.respond("query", opts.requestId, result !== undefined ? result : null);\n'
188
+ + ' } catch (e) {\n'
189
+ + ' bw._bwClient.respond("query", opts.requestId, null, e.message || String(e));\n'
190
+ + ' }\n'
191
+ + ' });\n'
192
+ + ' bw.registerRemote("_bw_listen", function(opts) {\n'
193
+ + ' if (!bw._bwClient) return;\n'
194
+ + ' if (!bw._bwClient._listeners) bw._bwClient._listeners = {};\n'
195
+ + ' var key = opts.selector + ":::" + opts.event;\n'
196
+ + ' if (bw._bwClient._listeners[key]) return;\n'
197
+ + ' var fn = function(e) {\n'
198
+ + ' var el = e.target.closest ? e.target.closest(opts.selector) : null;\n'
199
+ + ' if (!el) return;\n'
200
+ + ' bw._bwClient.respond("event", null, {\n'
201
+ + ' event: opts.event, selector: opts.selector, tagName: el.tagName,\n'
202
+ + ' id: el.id || null, text: (el.textContent || "").slice(0, 100)\n'
203
+ + ' });\n'
204
+ + ' };\n'
205
+ + ' document.addEventListener(opts.event, fn, true);\n'
206
+ + ' bw._bwClient._listeners[key] = { fn: fn, event: opts.event };\n'
207
+ + ' });\n'
208
+ + ' bw.registerRemote("_bw_unlisten", function(opts) {\n'
209
+ + ' if (!bw._bwClient || !bw._bwClient._listeners) return;\n'
210
+ + ' var key = opts.selector + ":::" + opts.event;\n'
211
+ + ' var entry = bw._bwClient._listeners[key];\n'
212
+ + ' if (!entry) return;\n'
213
+ + ' document.removeEventListener(entry.event, entry.fn, true);\n'
214
+ + ' delete bw._bwClient._listeners[key];\n'
104
215
  + ' });\n'
105
216
  + ' };\n'
106
217
  + '\n'
107
- + ' // ── Wire up data-bw-action click delegation ──\n'
218
+ + ' // ── Wire up action click delegation via bw_act_* classes ──\n'
108
219
  + ' _client._wireActions = function() {\n'
109
220
  + ' 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'
221
+ + ' var el = e.target;\n'
222
+ + ' var action = null;\n'
223
+ + ' while (el && el !== document) {\n'
224
+ + ' if (el.classList) {\n'
225
+ + ' for (var i = 0; i < el.classList.length; i++) {\n'
226
+ + ' if (el.classList[i].indexOf("bw_act_") === 0) {\n'
227
+ + ' action = el.classList[i].substring(7);\n'
228
+ + ' break;\n'
229
+ + ' }\n'
230
+ + ' }\n'
231
+ + ' }\n'
232
+ + ' if (action) break;\n'
233
+ + ' el = el.parentElement;\n'
234
+ + ' }\n'
235
+ + ' if (!action) return;\n'
112
236
  + ' e.preventDefault();\n'
113
237
  + ' 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'
238
+ + ' if (el.id) actionData.id = el.id;\n'
239
+ + ' var form = el.closest ? (el.closest("div") || document) : document;\n'
116
240
  + ' var inp = form.querySelector("input[type=text],input:not([type])");\n'
117
241
  + ' if (inp) { actionData.inputValue = inp.value; inp.value = ""; }\n'
118
- + ' _client.sendAction(el.getAttribute("data-bw-action"), actionData);\n'
242
+ + ' _client.sendAction(action, actionData);\n'
119
243
  + ' });\n'
120
244
  + ' document.addEventListener("keydown", function(e) {\n'
121
245
  + ' 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'
246
+ + ' var form = e.target.closest ? (e.target.closest("div") || document) : document;\n'
247
+ + ' var btn = null;\n'
248
+ + ' var els = form.querySelectorAll("[class*=bw_act_]");\n'
249
+ + ' if (els.length) btn = els[0];\n'
250
+ + ' var action = null;\n'
251
+ + ' if (btn && btn.classList) {\n'
252
+ + ' for (var i = 0; i < btn.classList.length; i++) {\n'
253
+ + ' if (btn.classList[i].indexOf("bw_act_") === 0) {\n'
254
+ + ' action = btn.classList[i].substring(7);\n'
255
+ + ' break;\n'
256
+ + ' }\n'
257
+ + ' }\n'
258
+ + ' if (action) {\n'
259
+ + ' _client.sendAction(action, { inputValue: e.target.value });\n'
260
+ + ' e.target.value = "";\n'
261
+ + ' }\n'
127
262
  + ' }\n'
128
263
  + ' }\n'
129
264
  + ' });\n'
130
265
  + ' };\n'
131
266
  + '\n'
267
+ + ' // ── Wire bw.remote for server communication ──\n'
268
+ + ' bw.remote = {\n'
269
+ + ' send: function(msg) { _client.respond("action", null, msg); }\n'
270
+ + ' };\n'
271
+ + '\n'
132
272
  + ' // ── Event delegation helper ──\n'
133
273
  + ' _client.listen = function(selector, event, action) {\n'
134
274
  + ' document.addEventListener(event, function(e) {\n'
@@ -22,7 +22,6 @@ import { VERSION } from '../version.js';
22
22
  * @param {string} [opts.title='bwserve'] - Page title
23
23
  * @param {string} [opts.theme] - Theme preset name or config
24
24
  * @param {boolean} [opts.injectBitwrench=true] - Whether to inject bitwrench scripts
25
- * @param {boolean} [opts.allowExec=false] - Enable exec message type
26
25
  * @returns {string} Complete HTML document
27
26
  */
28
27
  export function generateShell(opts) {
@@ -78,9 +77,6 @@ export function generateShell(opts) {
78
77
  script.push('(function() {');
79
78
  script.push(' "use strict";');
80
79
  script.push(' var clientId = ' + JSON.stringify(clientId) + ';');
81
- if (opts.allowExec) {
82
- script.push(' bw._allowExec = true;');
83
- }
84
80
  script.push(' bw._bwClient.id = clientId;');
85
81
  script.push(' bw._bwClient._registerBuiltins();');
86
82
  script.push(' bw._bwClient._wireActions();');
@@ -4,15 +4,17 @@
4
4
  * Represents one browser tab connected via SSE. The server calls methods
5
5
  * on this object to push UI updates to the client.
6
6
  *
7
- * Protocol message types (sent as SSE data):
8
- * { type: 'replace', target: '#app', node: {t,a,c,o} }
9
- * { type: 'append', target: '#list', node: {t,a,c,o} }
10
- * { type: 'remove', target: '#item-3' }
11
- * { type: 'patch', target: 'bw_counter_abc', content: '42', attr: null }
12
- * { type: 'batch', ops: [ ...messages ] }
13
- * { type: 'register', name: 'fn', body: 'function(x) { ... }' }
14
- * { type: 'call', name: 'fn', args: [...] }
15
- * { type: 'exec', code: 'js code string' }
7
+ * Protocol message types v2.1 (sent as SSE data, all stamped v:1):
8
+ * { v: 1, type: 'hello' } handshake
9
+ * { v: 1, type: 'mount', ref: '#app', taco: {t,a,c,o} } — mount TACO
10
+ * { v: 1, type: 'append', ref: '#list', taco: {t,a,c,o} }
11
+ * { v: 1, type: 'remove', ref: '#item-3' }
12
+ * { v: 1, type: 'patch', ref: '#id', text: '42' } — discriminated patch
13
+ * { v: 1, type: 'batch', ops: [ ...messages ] }
14
+ * { v: 1, type: 'call', name: 'fn', args: [...] }
15
+ * { v: 1, type: 'listen', topic: 'bw:lifecycle' }
16
+ *
17
+ * Removed in 2.1 (code-bearing): register, exec, query
16
18
  *
17
19
  * @module bwserve/client
18
20
  */
@@ -30,28 +32,44 @@ export class BwServeClient {
30
32
  this._res = res; // SSE response stream (null in stub)
31
33
  this._handlers = {}; // action name → handler
32
34
  this._closed = false;
33
- this._pending = {}; // requestId → { resolve, reject, timer }
34
35
  }
35
36
 
36
37
  /**
37
- * Replace the content of a DOM element with a TACO.
38
+ * Mount a TACO at the given selector (2.1 verb: "mount").
39
+ * Replaces the content of the target element.
38
40
  *
39
- * @param {string} selector - CSS selector or UUID
40
- * @param {Object} taco - TACO object to render
41
+ * @param {string} selector - CSS selector or UUID (the "ref")
42
+ * @param {Object} taco - TACO object to mount
43
+ */
44
+ mount(selector, taco) {
45
+ this._send({ type: 'mount', ref: selector, taco: taco });
46
+ }
47
+
48
+ /**
49
+ * Alias for mount() — backward compatibility with 2.0.x render().
50
+ * @deprecated Use mount() instead.
41
51
  */
42
52
  render(selector, taco) {
43
- this._send({ type: 'replace', target: selector, node: taco });
53
+ this.mount(selector, taco);
44
54
  }
45
55
 
46
56
  /**
47
57
  * Patch an element's content or attributes without rebuild.
48
- *
49
- * @param {string} id - Element UUID (from bw.uuid())
50
- * @param {string} content - New text content
51
- * @param {Object} [attr] - Attributes to update
52
- */
53
- patch(id, content, attr) {
54
- this._send({ type: 'patch', target: id, content, attr: attr || null });
58
+ * 2.1 uses discriminated fields: the patch object's keys (text, attrs,
59
+ * content, etc.) are spread directly into the message.
60
+ *
61
+ * @param {string} ref - CSS selector or element UUID
62
+ * @param {Object} fields - Discriminated patch fields (e.g. {text:'hi'}, {attrs:{class:'x'}})
63
+ */
64
+ patch(ref, fields) {
65
+ var msg = { type: 'patch', ref: ref };
66
+ if (fields && typeof fields === 'object') {
67
+ var keys = Object.keys(fields);
68
+ for (var i = 0; i < keys.length; i++) {
69
+ msg[keys[i]] = fields[keys[i]];
70
+ }
71
+ }
72
+ this._send(msg);
55
73
  }
56
74
 
57
75
  /**
@@ -61,7 +79,7 @@ export class BwServeClient {
61
79
  * @param {Object} taco - TACO object to append
62
80
  */
63
81
  append(selector, taco) {
64
- this._send({ type: 'append', target: selector, node: taco });
82
+ this._send({ type: 'append', ref: selector, taco: taco });
65
83
  }
66
84
 
67
85
  /**
@@ -70,40 +88,27 @@ export class BwServeClient {
70
88
  * @param {string} selector - CSS selector or UUID of element to remove
71
89
  */
72
90
  remove(selector) {
73
- this._send({ type: 'remove', target: selector });
91
+ this._send({ type: 'remove', ref: selector });
74
92
  }
75
93
 
76
94
  /**
77
95
  * Send multiple operations as a single batch.
78
96
  *
79
- * @param {Array} ops - Array of message objects (replace/append/remove/patch)
97
+ * @param {Array} ops - Array of message objects
80
98
  */
81
99
  batch(ops) {
82
- this._send({ type: 'batch', ops });
100
+ this._send({ type: 'batch', ops: ops });
83
101
  }
84
102
 
85
103
  /**
86
104
  * Send a bw.message() dispatch to a tagged component on the client.
87
105
  *
88
- * @param {string} target - Component userTag or UUID
106
+ * @param {string} ref - Component userTag or UUID
89
107
  * @param {string} action - Method name to call
90
108
  * @param {*} data - Data to pass to the method
91
109
  */
92
- message(target, action, data) {
93
- this._send({ type: 'message', target, action, data });
94
- }
95
-
96
- /**
97
- * Register a named function on the client for later invocation via call().
98
- *
99
- * The function body is sent as a string and compiled on the client side.
100
- * Registered functions persist for the lifetime of the connection.
101
- *
102
- * @param {string} name - Function name (used as key for later call())
103
- * @param {string} body - Function source as string, e.g. "function(el) { el.scrollTop = el.scrollHeight; }"
104
- */
105
- register(name, body) {
106
- this._send({ type: 'register', name, body });
110
+ message(ref, action, data) {
111
+ this._send({ type: 'message', ref: ref, action: action, data: data });
107
112
  }
108
113
 
109
114
  /**
@@ -116,19 +121,21 @@ export class BwServeClient {
116
121
  * @param {...*} args - Arguments to pass to the function
117
122
  */
118
123
  call(name, ...args) {
119
- this._send({ type: 'call', name, args });
124
+ this._send({ type: 'call', name: name, args: args });
120
125
  }
121
126
 
122
127
  /**
123
- * Execute arbitrary JavaScript code on the client.
128
+ * Subscribe to a client-side topic. The client will forward matching
129
+ * events back through the return route.
124
130
  *
125
- * Requires the client connection to be created with { allowExec: true }.
126
- * Use call() as the safe alternative when possible.
127
- *
128
- * @param {string} code - JavaScript code string to execute
131
+ * @param {string} topic - Topic name (e.g. 'bw:lifecycle')
132
+ * @param {Function} handler - Called with (data) when topic events arrive
133
+ * @returns {BwServeClient} this (for chaining)
129
134
  */
130
- exec(code) {
131
- this._send({ type: 'exec', code });
135
+ listen(topic, handler) {
136
+ this._handlers['_topic:' + topic] = handler;
137
+ this._send({ type: 'listen', topic: topic });
138
+ return this;
132
139
  }
133
140
 
134
141
  /**
@@ -155,10 +162,13 @@ export class BwServeClient {
155
162
 
156
163
  /**
157
164
  * Send a protocol message to the client via SSE.
165
+ * All messages are stamped with v: 1 (wire protocol version).
158
166
  * @private
159
167
  */
160
168
  _send(msg) {
161
169
  if (this._closed) return;
170
+ // Stamp the wire protocol version
171
+ msg.v = 1;
162
172
  // Always store for testing / inspection
163
173
  if (!this._sent) this._sent = [];
164
174
  this._sent.push(msg);
@@ -172,177 +182,6 @@ export class BwServeClient {
172
182
  }
173
183
  }
174
184
 
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
- // ── Inspect ──
267
-
268
- /**
269
- * Inspect the DOM tree of the connected client.
270
- *
271
- * Calls the `_bw_tree` builtin on the client which delegates to
272
- * `bw.inspect()` when available, returning a plain-object tree with
273
- * bitwrench metadata (tag, uuid, type, handles, state, children).
274
- *
275
- * @param {string} [selector='body'] - CSS selector of root element
276
- * @param {Object} [options]
277
- * @param {number} [options.depth=3] - Max recursion depth
278
- * @param {number} [options.timeout=10000] - Timeout in ms
279
- * @returns {Promise<Object|null>} Tree object, or null if element not found
280
- */
281
- inspect(selector, options) {
282
- var opts = options || {};
283
- var pend = this._pend(opts.timeout || 10000);
284
- this.call('_bw_tree', {
285
- selector: selector || 'body',
286
- depth: opts.depth || 3,
287
- requestId: pend.requestId
288
- });
289
- return pend.promise;
290
- }
291
-
292
- // ── Screenshot ──
293
-
294
- /**
295
- * Capture a screenshot of the client's page or a specific element.
296
- *
297
- * Requires the server to be created with `{ allowScreenshot: true }`.
298
- * Uses html2canvas on the client side (lazy-loaded on first call).
299
- *
300
- * @param {string} [selector='body'] - CSS selector of element to capture
301
- * @param {Object} [options]
302
- * @param {string} [options.format='png'] - 'png' or 'jpeg'
303
- * @param {number} [options.quality=0.85] - JPEG quality 0-1 (ignored for PNG)
304
- * @param {number} [options.maxWidth] - Resize if wider (preserves aspect ratio)
305
- * @param {number} [options.maxHeight] - Resize if taller (preserves aspect ratio)
306
- * @param {number} [options.scale=1] - Device pixel ratio override
307
- * @param {number} [options.timeout=10000] - Reject after ms
308
- * @returns {Promise<Object>} { data: Buffer, width, height, format }
309
- */
310
- screenshot(selector, options) {
311
- var self = this;
312
- var opts = options || {};
313
- var timeout = opts.timeout || 10000;
314
-
315
- if (!self._allowScreenshot) {
316
- return Promise.reject(new Error('Screenshot not enabled. Set allowScreenshot: true in server options.'));
317
- }
318
-
319
- var pend = self._pend(timeout);
320
-
321
- // Call the bwclient-registered capture function
322
- self.call('_bw_screenshot', {
323
- requestId: pend.requestId,
324
- selector: selector || 'body',
325
- format: opts.format || 'png',
326
- quality: opts.quality || 0.85,
327
- maxWidth: opts.maxWidth || null,
328
- maxHeight: opts.maxHeight || null,
329
- scale: opts.scale || 1,
330
- captureUrl: '/bw/lib/vendor/html2canvas.min.js'
331
- });
332
-
333
- // Transform the raw response into { data: Buffer, width, height, format }
334
- return pend.promise.then(function(result) {
335
- if (!result || !result.data) return result;
336
- var base64 = result.data.split(',')[1];
337
- return {
338
- data: Buffer.from(base64, 'base64'),
339
- width: result.width,
340
- height: result.height,
341
- format: result.format
342
- };
343
- });
344
- }
345
-
346
185
  /**
347
186
  * Dispatch an incoming action from the client.
348
187
  * @private