bitwrench 2.0.15 → 2.0.17

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 (53) hide show
  1. package/README.md +57 -21
  2. package/dist/bitwrench-bccl.cjs.js +3750 -0
  3. package/dist/bitwrench-bccl.cjs.min.js +40 -0
  4. package/dist/bitwrench-bccl.esm.js +3745 -0
  5. package/dist/bitwrench-bccl.esm.min.js +40 -0
  6. package/dist/bitwrench-bccl.umd.js +3756 -0
  7. package/dist/bitwrench-bccl.umd.min.js +40 -0
  8. package/dist/bitwrench-code-edit.cjs.js +57 -7
  9. package/dist/bitwrench-code-edit.cjs.min.js +9 -2
  10. package/dist/bitwrench-code-edit.es5.js +74 -11
  11. package/dist/bitwrench-code-edit.es5.min.js +9 -2
  12. package/dist/bitwrench-code-edit.esm.js +57 -7
  13. package/dist/bitwrench-code-edit.esm.min.js +9 -2
  14. package/dist/bitwrench-code-edit.umd.js +57 -7
  15. package/dist/bitwrench-code-edit.umd.min.js +9 -2
  16. package/dist/bitwrench-lean.cjs.js +905 -157
  17. package/dist/bitwrench-lean.cjs.min.js +7 -7
  18. package/dist/bitwrench-lean.es5.js +931 -157
  19. package/dist/bitwrench-lean.es5.min.js +5 -5
  20. package/dist/bitwrench-lean.esm.js +904 -157
  21. package/dist/bitwrench-lean.esm.min.js +7 -7
  22. package/dist/bitwrench-lean.umd.js +905 -157
  23. package/dist/bitwrench-lean.umd.min.js +7 -7
  24. package/dist/bitwrench.cjs.js +910 -158
  25. package/dist/bitwrench.cjs.min.js +8 -8
  26. package/dist/bitwrench.css +60 -17
  27. package/dist/bitwrench.es5.js +939 -158
  28. package/dist/bitwrench.es5.min.js +6 -6
  29. package/dist/bitwrench.esm.js +909 -158
  30. package/dist/bitwrench.esm.min.js +8 -8
  31. package/dist/bitwrench.min.css +1 -1
  32. package/dist/bitwrench.umd.js +910 -158
  33. package/dist/bitwrench.umd.min.js +8 -8
  34. package/dist/builds.json +168 -80
  35. package/dist/bwserve.cjs.js +660 -0
  36. package/dist/bwserve.esm.js +652 -0
  37. package/dist/sri.json +36 -28
  38. package/package.json +20 -3
  39. package/readme.html +62 -23
  40. package/src/bitwrench-bccl-entry.js +72 -0
  41. package/src/bitwrench-bccl.js +5 -1
  42. package/src/bitwrench-code-edit.js +56 -6
  43. package/src/bitwrench-color-utils.js +5 -6
  44. package/src/bitwrench-styles.js +20 -8
  45. package/src/bitwrench.js +876 -140
  46. package/src/bwserve/client.js +182 -0
  47. package/src/bwserve/index.js +363 -0
  48. package/src/bwserve/shell.js +106 -0
  49. package/src/cli/index.js +36 -15
  50. package/src/cli/layout-default.js +47 -32
  51. package/src/cli/serve.js +325 -0
  52. package/src/version.js +3 -3
  53. /package/bin/{bitwrench.js → bwcli.js} +0 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * BwServeClient — per-client connection for bwserve.
3
+ *
4
+ * Represents one browser tab connected via SSE. The server calls methods
5
+ * on this object to push UI updates to the client.
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' }
16
+ *
17
+ * @module bwserve/client
18
+ */
19
+
20
+ /**
21
+ * BwServeClient — one connected browser tab.
22
+ */
23
+ export class BwServeClient {
24
+ constructor(id, res) {
25
+ this.id = id;
26
+ this._res = res; // SSE response stream (null in stub)
27
+ this._handlers = {}; // action name → handler
28
+ this._closed = false;
29
+ }
30
+
31
+ /**
32
+ * Replace the content of a DOM element with a TACO.
33
+ *
34
+ * @param {string} selector - CSS selector or UUID
35
+ * @param {Object} taco - TACO object to render
36
+ */
37
+ render(selector, taco) {
38
+ this._send({ type: 'replace', target: selector, node: taco });
39
+ }
40
+
41
+ /**
42
+ * Patch an element's content or attributes without rebuild.
43
+ *
44
+ * @param {string} id - Element UUID (from bw.uuid())
45
+ * @param {string} content - New text content
46
+ * @param {Object} [attr] - Attributes to update
47
+ */
48
+ patch(id, content, attr) {
49
+ this._send({ type: 'patch', target: id, content, attr: attr || null });
50
+ }
51
+
52
+ /**
53
+ * Append a TACO as a new child of the target element.
54
+ *
55
+ * @param {string} selector - CSS selector of parent
56
+ * @param {Object} taco - TACO object to append
57
+ */
58
+ append(selector, taco) {
59
+ this._send({ type: 'append', target: selector, node: taco });
60
+ }
61
+
62
+ /**
63
+ * Remove an element from the DOM (with cleanup).
64
+ *
65
+ * @param {string} selector - CSS selector or UUID of element to remove
66
+ */
67
+ remove(selector) {
68
+ this._send({ type: 'remove', target: selector });
69
+ }
70
+
71
+ /**
72
+ * Send multiple operations as a single batch.
73
+ *
74
+ * @param {Array} ops - Array of message objects (replace/append/remove/patch)
75
+ */
76
+ batch(ops) {
77
+ this._send({ type: 'batch', ops });
78
+ }
79
+
80
+ /**
81
+ * Send a bw.message() dispatch to a tagged component on the client.
82
+ *
83
+ * @param {string} target - Component userTag or UUID
84
+ * @param {string} action - Method name to call
85
+ * @param {*} data - Data to pass to the method
86
+ */
87
+ message(target, action, data) {
88
+ this._send({ type: 'message', target, action, data });
89
+ }
90
+
91
+ /**
92
+ * Register a named function on the client for later invocation via call().
93
+ *
94
+ * The function body is sent as a string and compiled on the client side.
95
+ * Registered functions persist for the lifetime of the connection.
96
+ *
97
+ * @param {string} name - Function name (used as key for later call())
98
+ * @param {string} body - Function source as string, e.g. "function(el) { el.scrollTop = el.scrollHeight; }"
99
+ */
100
+ register(name, body) {
101
+ this._send({ type: 'register', name, body });
102
+ }
103
+
104
+ /**
105
+ * Call a previously registered or built-in function on the client.
106
+ *
107
+ * Built-in functions (always available, no registration needed):
108
+ * scrollTo, focus, download, clipboard, redirect, log
109
+ *
110
+ * @param {string} name - Function name (registered or built-in)
111
+ * @param {...*} args - Arguments to pass to the function
112
+ */
113
+ call(name, ...args) {
114
+ this._send({ type: 'call', name, args });
115
+ }
116
+
117
+ /**
118
+ * Execute arbitrary JavaScript code on the client.
119
+ *
120
+ * Requires the client connection to be created with { allowExec: true }.
121
+ * Use call() as the safe alternative when possible.
122
+ *
123
+ * @param {string} code - JavaScript code string to execute
124
+ */
125
+ exec(code) {
126
+ this._send({ type: 'exec', code });
127
+ }
128
+
129
+ /**
130
+ * Register a handler for client actions (button clicks, form submits, etc.).
131
+ *
132
+ * @param {string} action - Action name (from o.events declarative handler)
133
+ * @param {Function} handler - Called with (data, client)
134
+ * @returns {BwServeClient} this (for chaining)
135
+ */
136
+ on(action, handler) {
137
+ this._handlers[action] = handler;
138
+ return this;
139
+ }
140
+
141
+ /**
142
+ * Close the SSE connection to this client.
143
+ */
144
+ close() {
145
+ this._closed = true;
146
+ if (this._res && typeof this._res.end === 'function') {
147
+ try { this._res.end(); } catch (e) { /* ignore */ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Send a protocol message to the client via SSE.
153
+ * @private
154
+ */
155
+ _send(msg) {
156
+ if (this._closed) return;
157
+ // Always store for testing / inspection
158
+ if (!this._sent) this._sent = [];
159
+ this._sent.push(msg);
160
+ // Write SSE frame if we have a live response stream
161
+ if (this._res && typeof this._res.write === 'function') {
162
+ try {
163
+ this._res.write('data: ' + JSON.stringify(msg) + '\n\n');
164
+ } catch (e) {
165
+ // Stream may have been closed — ignore write errors
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Dispatch an incoming action from the client.
172
+ * @private
173
+ */
174
+ _dispatch(action, data) {
175
+ const handler = this._handlers[action];
176
+ if (handler) {
177
+ handler(data, this);
178
+ return true;
179
+ }
180
+ return false;
181
+ }
182
+ }
@@ -0,0 +1,363 @@
1
+ /**
2
+ * bwserve — Server-driven UI library for bitwrench
3
+ *
4
+ * Programmatic API for building server-push UIs (Streamlit-style).
5
+ * Uses SSE (Server-Sent Events) by default, with WebSocket opt-in.
6
+ * Zero runtime dependencies — only Node.js stdlib (http, fs, path).
7
+ *
8
+ * Usage:
9
+ * import bwserve from 'bitwrench/bwserve';
10
+ * const app = bwserve.create({ port: 7902 });
11
+ * app.page('/', (client) => {
12
+ * client.render('#app', bw.makeCard({ title: 'Hello' }));
13
+ * });
14
+ * app.listen();
15
+ *
16
+ * @module bwserve
17
+ */
18
+
19
+ import { BwServeClient } from './client.js';
20
+ import { generateShell } from './shell.js';
21
+
22
+ // Resolve dist/ paths relative to the package root
23
+ import { fileURLToPath } from 'url';
24
+ import { dirname, resolve, join, extname } from 'path';
25
+ import { createServer } from 'http';
26
+ import { readFileSync, existsSync, statSync } from 'fs';
27
+
28
+ var __dirname = dirname(fileURLToPath(import.meta.url));
29
+
30
+ // Resolve dist/ — try source layout (src/bwserve/), then npm install layout,
31
+ // then dist/ itself (when running from dist/bwserve.esm.js)
32
+ var DIST_DIR = resolve(__dirname, '..', '..', 'dist');
33
+ if (!existsSync(DIST_DIR)) {
34
+ DIST_DIR = resolve(__dirname, '..', 'dist');
35
+ }
36
+ if (!existsSync(DIST_DIR)) {
37
+ DIST_DIR = __dirname;
38
+ }
39
+
40
+ // MIME type lookup for static file serving
41
+ var MIME_TYPES = {
42
+ '.html': 'text/html; charset=utf-8',
43
+ '.js': 'application/javascript; charset=utf-8',
44
+ '.css': 'text/css; charset=utf-8',
45
+ '.json': 'application/json; charset=utf-8',
46
+ '.png': 'image/png',
47
+ '.jpg': 'image/jpeg',
48
+ '.jpeg': 'image/jpeg',
49
+ '.gif': 'image/gif',
50
+ '.svg': 'image/svg+xml',
51
+ '.ico': 'image/x-icon',
52
+ '.woff': 'font/woff',
53
+ '.woff2': 'font/woff2',
54
+ '.ttf': 'font/ttf',
55
+ '.map': 'application/json'
56
+ };
57
+
58
+ /**
59
+ * Create a bwserve application.
60
+ *
61
+ * @param {Object} opts - Server options
62
+ * @param {number} [opts.port=7902] - Port to listen on
63
+ * @param {string} [opts.title='bwserve'] - Page title
64
+ * @param {string} [opts.static] - Directory to serve static files from
65
+ * @param {boolean} [opts.injectBitwrench=true] - Auto-inject bitwrench client JS
66
+ * @param {string|Object} [opts.theme] - Theme preset name or config object
67
+ * @returns {BwServeApp} Application instance
68
+ */
69
+ export function create(opts) {
70
+ return new BwServeApp(opts || {});
71
+ }
72
+
73
+ /**
74
+ * BwServeApp — the server application object.
75
+ *
76
+ * Manages pages, client connections, and the HTTP/SSE server.
77
+ */
78
+ class BwServeApp {
79
+ constructor(opts) {
80
+ this.port = opts.port || 7902;
81
+ this.title = opts.title || 'bwserve';
82
+ this.staticDir = opts.static || null;
83
+ this.injectBitwrench = opts.injectBitwrench !== false;
84
+ this.theme = opts.theme || null;
85
+ this.allowExec = opts.allowExec || false;
86
+ this.keepAliveInterval = opts.keepAliveInterval || 15000;
87
+ this._pages = new Map();
88
+ this._clients = new Map();
89
+ this._server = null;
90
+ this._clientCounter = 0;
91
+ }
92
+
93
+ /**
94
+ * Register a page handler.
95
+ *
96
+ * @param {string} path - URL path (e.g., '/', '/dashboard')
97
+ * @param {Function} handler - Called with (client: BwServeClient) on connection
98
+ * @returns {BwServeApp} this (for chaining)
99
+ */
100
+ page(path, handler) {
101
+ this._pages.set(path, handler);
102
+ return this;
103
+ }
104
+
105
+ /**
106
+ * Start the HTTP server and begin accepting SSE connections.
107
+ *
108
+ * @param {Function} [callback] - Called when server is listening
109
+ * @returns {Promise<void>}
110
+ */
111
+ listen(callback) {
112
+ var self = this;
113
+
114
+ return new Promise(function(res) {
115
+ self._server = createServer(function(req, rawRes) {
116
+ self._handleRequest(req, rawRes);
117
+ });
118
+
119
+ self._server.listen(self.port, function() {
120
+ if (callback) callback();
121
+ res();
122
+ });
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Stop the server and close all client connections.
128
+ */
129
+ close() {
130
+ var self = this;
131
+ return new Promise(function(res) {
132
+ // Close all SSE streams
133
+ for (var record of self._clients.values()) {
134
+ if (record.client && typeof record.client.close === 'function') {
135
+ record.client.close();
136
+ }
137
+ }
138
+ self._clients.clear();
139
+
140
+ if (self._server) {
141
+ self._server.close(function() {
142
+ self._server = null;
143
+ res();
144
+ });
145
+ } else {
146
+ res();
147
+ }
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Get count of active client connections.
153
+ * @returns {number}
154
+ */
155
+ get clientCount() {
156
+ return this._clients.size;
157
+ }
158
+
159
+ /**
160
+ * Broadcast a protocol message to all connected clients.
161
+ *
162
+ * If msg has a clientId field, send only to that client.
163
+ * Otherwise, broadcast to all.
164
+ *
165
+ * @param {Object} msg - Protocol message (replace, patch, append, remove, batch)
166
+ * @returns {number} Number of clients that received the message
167
+ */
168
+ broadcast(msg) {
169
+ if (msg.clientId) {
170
+ var record = this._clients.get(msg.clientId);
171
+ if (record && record.client) {
172
+ record.client._send(msg);
173
+ return 1;
174
+ }
175
+ return 0;
176
+ }
177
+ var count = 0;
178
+ for (var record of this._clients.values()) {
179
+ if (record.client && !record.client._closed) {
180
+ record.client._send(msg);
181
+ count++;
182
+ }
183
+ }
184
+ return count;
185
+ }
186
+
187
+ /**
188
+ * Internal: route incoming HTTP requests.
189
+ * @private
190
+ */
191
+ _handleRequest(req, res) {
192
+ var url = req.url || '/';
193
+ var method = req.method || 'GET';
194
+
195
+ // Parse URL path (strip query string)
196
+ var path = url.split('?')[0];
197
+
198
+ // /__bw/bitwrench.umd.js — serve bitwrench client library
199
+ if (path === '/__bw/bitwrench.umd.js' && method === 'GET') {
200
+ return this._serveDistFile(res, 'bitwrench.umd.js');
201
+ }
202
+
203
+ // /__bw/bitwrench.umd.min.js — serve minified
204
+ if (path === '/__bw/bitwrench.umd.min.js' && method === 'GET') {
205
+ return this._serveDistFile(res, 'bitwrench.umd.min.js');
206
+ }
207
+
208
+ // /__bw/bitwrench.css — serve bitwrench CSS
209
+ if (path === '/__bw/bitwrench.css' && method === 'GET') {
210
+ return this._serveDistFile(res, 'bitwrench.css');
211
+ }
212
+
213
+ // /__bw/events/:clientId — SSE stream
214
+ if (path.startsWith('/__bw/events/') && method === 'GET') {
215
+ var clientId = path.slice('/__bw/events/'.length);
216
+ return this._handleSSE(req, res, clientId);
217
+ }
218
+
219
+ // /__bw/action/:clientId — action POST
220
+ if (path.startsWith('/__bw/action/') && method === 'POST') {
221
+ var actionClientId = path.slice('/__bw/action/'.length);
222
+ return this._handleAction(req, res, actionClientId);
223
+ }
224
+
225
+ // Registered page routes — serve shell HTML
226
+ if (method === 'GET' && this._pages.has(path)) {
227
+ var clientId2 = 'c' + (++this._clientCounter);
228
+ var shell = generateShell({
229
+ clientId: clientId2,
230
+ title: this.title,
231
+ theme: this.theme,
232
+ injectBitwrench: this.injectBitwrench,
233
+ allowExec: this.allowExec
234
+ });
235
+ // Store the page path for this client so SSE knows which handler to call
236
+ this._clients.set(clientId2, { pagePath: path, client: null });
237
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
238
+ res.end(shell);
239
+ return;
240
+ }
241
+
242
+ // Static file serving
243
+ if (method === 'GET' && this.staticDir) {
244
+ var filePath = join(this.staticDir, path);
245
+ if (existsSync(filePath) && statSync(filePath).isFile()) {
246
+ var ext = extname(filePath);
247
+ var mime = MIME_TYPES[ext] || 'application/octet-stream';
248
+ var content = readFileSync(filePath);
249
+ res.writeHead(200, { 'Content-Type': mime });
250
+ res.end(content);
251
+ return;
252
+ }
253
+ }
254
+
255
+ // 404
256
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
257
+ res.end('Not Found');
258
+ }
259
+
260
+ /**
261
+ * Serve a file from the dist/ directory.
262
+ * @private
263
+ */
264
+ _serveDistFile(res, filename) {
265
+ var filePath = join(DIST_DIR, filename);
266
+ if (!existsSync(filePath)) {
267
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
268
+ res.end('Not Found: ' + filename);
269
+ return;
270
+ }
271
+ var ext = extname(filename);
272
+ var mime = MIME_TYPES[ext] || 'application/octet-stream';
273
+ var content = readFileSync(filePath);
274
+ res.writeHead(200, {
275
+ 'Content-Type': mime,
276
+ 'Cache-Control': 'public, max-age=3600'
277
+ });
278
+ res.end(content);
279
+ }
280
+
281
+ /**
282
+ * Handle an SSE connection.
283
+ * @private
284
+ */
285
+ _handleSSE(req, res, clientId) {
286
+ var self = this;
287
+
288
+ // Set SSE headers
289
+ res.writeHead(200, {
290
+ 'Content-Type': 'text/event-stream',
291
+ 'Cache-Control': 'no-cache',
292
+ 'Connection': 'keep-alive',
293
+ 'Access-Control-Allow-Origin': '*'
294
+ });
295
+
296
+ // Create client instance
297
+ var client = new BwServeClient(clientId, res);
298
+
299
+ // Look up the pending client record (set during page serve)
300
+ var pending = self._clients.get(clientId);
301
+ var pagePath = pending ? pending.pagePath : '/';
302
+ self._clients.set(clientId, { pagePath: pagePath, client: client });
303
+
304
+ // Keep-alive: send SSE comment periodically
305
+ var keepAlive = setInterval(function() {
306
+ if (!client._closed) {
307
+ try { res.write(':keepalive\n\n'); } catch (e) { /* ignore */ }
308
+ }
309
+ }, self.keepAliveInterval);
310
+
311
+ // Clean up on disconnect
312
+ req.on('close', function() {
313
+ clearInterval(keepAlive);
314
+ client._closed = true;
315
+ self._clients.delete(clientId);
316
+ });
317
+
318
+ // Call the page handler
319
+ var handler = self._pages.get(pagePath);
320
+ if (handler) {
321
+ try {
322
+ handler(client);
323
+ } catch (e) {
324
+ console.error('[bwserve] Page handler error:', e);
325
+ }
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Handle an action POST from a client.
331
+ * @private
332
+ */
333
+ _handleAction(req, res, clientId) {
334
+ var record = this._clients.get(clientId);
335
+ if (!record || !record.client) {
336
+ res.writeHead(404, { 'Content-Type': 'application/json' });
337
+ res.end(JSON.stringify({ error: 'Unknown client' }));
338
+ return;
339
+ }
340
+
341
+ var body = '';
342
+ req.on('data', function(chunk) {
343
+ body += chunk;
344
+ });
345
+ req.on('end', function() {
346
+ try {
347
+ var data = JSON.parse(body);
348
+ var action = data.action;
349
+ var payload = data.data || data;
350
+ record.client._dispatch(action, payload);
351
+ res.writeHead(200, { 'Content-Type': 'application/json' });
352
+ res.end(JSON.stringify({ ok: true }));
353
+ } catch (e) {
354
+ res.writeHead(400, { 'Content-Type': 'application/json' });
355
+ res.end(JSON.stringify({ error: e.message }));
356
+ }
357
+ });
358
+ }
359
+ }
360
+
361
+ export { BwServeApp, BwServeClient };
362
+
363
+ export default { create, BwServeApp, BwServeClient };
@@ -0,0 +1,106 @@
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/ routes
6
+ * - Calls bw.loadDefaultStyles()
7
+ * - Optionally applies a theme
8
+ * - Creates a #app div
9
+ * - Opens an SSE connection via bw.clientConnect()
10
+ * - Delegates data-bw-action clicks to the server via POST
11
+ *
12
+ * @module bwserve/shell
13
+ */
14
+
15
+ /**
16
+ * Generate the shell HTML page for a bwserve app.
17
+ *
18
+ * @param {Object} opts
19
+ * @param {string} opts.clientId - Unique client ID for this connection
20
+ * @param {string} [opts.title='bwserve'] - Page title
21
+ * @param {string} [opts.theme] - Theme preset name or config
22
+ * @param {boolean} [opts.injectBitwrench=true] - Whether to inject bitwrench scripts
23
+ * @returns {string} Complete HTML document
24
+ */
25
+ export function generateShell(opts) {
26
+ opts = opts || {};
27
+ var clientId = opts.clientId || 'default';
28
+ var title = opts.title || 'bwserve';
29
+ var inject = opts.injectBitwrench !== false;
30
+
31
+ var head = [
32
+ '<!DOCTYPE html>',
33
+ '<html lang="en">',
34
+ '<head>',
35
+ '<meta charset="UTF-8">',
36
+ '<meta name="viewport" content="width=device-width, initial-scale=1.0">',
37
+ '<title>' + title + '</title>'
38
+ ];
39
+
40
+ if (inject) {
41
+ head.push('<script src="/__bw/bitwrench.umd.js"></script>');
42
+ head.push('<link rel="stylesheet" href="/__bw/bitwrench.css">');
43
+ }
44
+
45
+ head.push('</head>');
46
+ head.push('<body>');
47
+ head.push('<div id="app"></div>');
48
+
49
+ var script = [
50
+ '<script>',
51
+ '(function() {',
52
+ ' "use strict";',
53
+ ' bw.loadDefaultStyles();'
54
+ ];
55
+
56
+ if (opts.theme) {
57
+ script.push(' bw.generateTheme("bwserve", ' + JSON.stringify(
58
+ typeof opts.theme === 'string'
59
+ ? { primary: '#006666', secondary: '#333333' }
60
+ : opts.theme
61
+ ) + ');');
62
+ }
63
+
64
+ script.push(' var clientId = ' + JSON.stringify(clientId) + ';');
65
+ script.push(' var conn = bw.clientConnect("/__bw/events/" + clientId, {');
66
+ script.push(' actionUrl: "/__bw/action/" + clientId,');
67
+ if (opts.allowExec) {
68
+ script.push(' allowExec: true,');
69
+ }
70
+ script.push(' onStatus: function(s) {');
71
+ script.push(' if (typeof console !== "undefined") console.log("[bwserve] " + s);');
72
+ script.push(' }');
73
+ script.push(' });');
74
+
75
+ // data-bw-action click delegation
76
+ script.push(' document.addEventListener("click", function(e) {');
77
+ script.push(' var el = e.target.closest ? e.target.closest("[data-bw-action]") : null;');
78
+ script.push(' if (!el) return;');
79
+ script.push(' e.preventDefault();');
80
+ script.push(' var actionData = {};');
81
+ script.push(' if (el.getAttribute("data-bw-id")) actionData.bwId = el.getAttribute("data-bw-id");');
82
+ script.push(' var form = el.closest("div") || document;');
83
+ script.push(' var inp = form.querySelector("input[type=text],input:not([type])");');
84
+ script.push(' if (inp) { actionData.inputValue = inp.value; inp.value = ""; }');
85
+ script.push(' conn.sendAction(el.getAttribute("data-bw-action"), actionData);');
86
+ script.push(' });');
87
+
88
+ // Enter key on inputs
89
+ script.push(' document.addEventListener("keydown", function(e) {');
90
+ script.push(' if (e.key === "Enter" && e.target.tagName === "INPUT") {');
91
+ script.push(' var form = e.target.closest("div") || document;');
92
+ script.push(' var btn = form.querySelector("[data-bw-action]");');
93
+ script.push(' if (btn) {');
94
+ script.push(' conn.sendAction(btn.getAttribute("data-bw-action"), { inputValue: e.target.value });');
95
+ script.push(' e.target.value = "";');
96
+ script.push(' }');
97
+ script.push(' }');
98
+ script.push(' });');
99
+
100
+ script.push('})();');
101
+ script.push('</script>');
102
+ script.push('</body>');
103
+ script.push('</html>');
104
+
105
+ return head.concat(script).join('\n');
106
+ }