bitwrench 2.0.14 → 2.0.16

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 (61) hide show
  1. package/README.md +57 -21
  2. package/dist/bitwrench-bccl.cjs.js +3746 -0
  3. package/dist/bitwrench-bccl.cjs.min.js +40 -0
  4. package/dist/bitwrench-bccl.esm.js +3741 -0
  5. package/dist/bitwrench-bccl.esm.min.js +40 -0
  6. package/dist/bitwrench-bccl.umd.js +3752 -0
  7. package/dist/bitwrench-bccl.umd.min.js +40 -0
  8. package/dist/bitwrench-code-edit.cjs.js +99 -49
  9. package/dist/bitwrench-code-edit.cjs.min.js +23 -0
  10. package/dist/bitwrench-code-edit.es5.js +79 -16
  11. package/dist/bitwrench-code-edit.es5.min.js +9 -2
  12. package/dist/bitwrench-code-edit.esm.js +99 -49
  13. package/dist/bitwrench-code-edit.esm.min.js +9 -2
  14. package/dist/bitwrench-code-edit.umd.js +99 -49
  15. package/dist/bitwrench-code-edit.umd.min.js +9 -2
  16. package/dist/bitwrench-lean.cjs.js +4923 -3248
  17. package/dist/bitwrench-lean.cjs.min.js +35 -6
  18. package/dist/bitwrench-lean.es5.js +6325 -4580
  19. package/dist/bitwrench-lean.es5.min.js +32 -3
  20. package/dist/bitwrench-lean.esm.js +4923 -3248
  21. package/dist/bitwrench-lean.esm.min.js +35 -6
  22. package/dist/bitwrench-lean.umd.js +4923 -3248
  23. package/dist/bitwrench-lean.umd.min.js +35 -6
  24. package/dist/bitwrench.cjs.js +5082 -3667
  25. package/dist/bitwrench.cjs.min.js +38 -8
  26. package/dist/bitwrench.css +2289 -6034
  27. package/dist/bitwrench.es5.js +6862 -5346
  28. package/dist/bitwrench.es5.min.js +34 -5
  29. package/dist/bitwrench.esm.js +5082 -3667
  30. package/dist/bitwrench.esm.min.js +38 -8
  31. package/dist/bitwrench.min.css +1 -0
  32. package/dist/bitwrench.umd.js +5082 -3667
  33. package/dist/bitwrench.umd.min.js +38 -8
  34. package/dist/builds.json +184 -74
  35. package/dist/bwserve.cjs.js +646 -0
  36. package/dist/bwserve.esm.js +638 -0
  37. package/dist/sri.json +36 -26
  38. package/package.json +23 -6
  39. package/readme.html +71 -32
  40. package/src/bitwrench-bccl-entry.js +72 -0
  41. package/src/{bitwrench-components-v2.js → bitwrench-bccl.js} +396 -647
  42. package/src/bitwrench-code-edit.js +98 -48
  43. package/src/bitwrench-color-utils.js +24 -18
  44. package/src/bitwrench-components-stub.js +4 -1
  45. package/src/bitwrench-file-ops.js +180 -0
  46. package/src/bitwrench-lean.js +2 -2
  47. package/src/bitwrench-styles.js +1287 -4029
  48. package/src/bitwrench-utils.js +458 -0
  49. package/src/bitwrench.js +2070 -1292
  50. package/src/bwserve/client.js +182 -0
  51. package/src/bwserve/index.js +352 -0
  52. package/src/bwserve/shell.js +103 -0
  53. package/src/cli/index.js +36 -15
  54. package/src/cli/layout-default.js +18 -18
  55. package/src/cli/serve.js +325 -0
  56. package/src/generate-css.js +73 -53
  57. package/src/version.js +3 -3
  58. package/src/bitwrench-component-base.js +0 -736
  59. package/src/bitwrench-components-inline.js +0 -374
  60. package/src/bitwrench-components.js +0 -610
  61. /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,352 @@
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
+ var DIST_DIR = resolve(__dirname, '..', '..', 'dist');
30
+
31
+ // MIME type lookup for static file serving
32
+ var MIME_TYPES = {
33
+ '.html': 'text/html; charset=utf-8',
34
+ '.js': 'application/javascript; charset=utf-8',
35
+ '.css': 'text/css; charset=utf-8',
36
+ '.json': 'application/json; charset=utf-8',
37
+ '.png': 'image/png',
38
+ '.jpg': 'image/jpeg',
39
+ '.jpeg': 'image/jpeg',
40
+ '.gif': 'image/gif',
41
+ '.svg': 'image/svg+xml',
42
+ '.ico': 'image/x-icon',
43
+ '.woff': 'font/woff',
44
+ '.woff2': 'font/woff2',
45
+ '.ttf': 'font/ttf',
46
+ '.map': 'application/json'
47
+ };
48
+
49
+ /**
50
+ * Create a bwserve application.
51
+ *
52
+ * @param {Object} opts - Server options
53
+ * @param {number} [opts.port=7902] - Port to listen on
54
+ * @param {string} [opts.title='bwserve'] - Page title
55
+ * @param {string} [opts.static] - Directory to serve static files from
56
+ * @param {boolean} [opts.injectBitwrench=true] - Auto-inject bitwrench client JS
57
+ * @param {string|Object} [opts.theme] - Theme preset name or config object
58
+ * @returns {BwServeApp} Application instance
59
+ */
60
+ export function create(opts) {
61
+ return new BwServeApp(opts || {});
62
+ }
63
+
64
+ /**
65
+ * BwServeApp — the server application object.
66
+ *
67
+ * Manages pages, client connections, and the HTTP/SSE server.
68
+ */
69
+ class BwServeApp {
70
+ constructor(opts) {
71
+ this.port = opts.port || 7902;
72
+ this.title = opts.title || 'bwserve';
73
+ this.staticDir = opts.static || null;
74
+ this.injectBitwrench = opts.injectBitwrench !== false;
75
+ this.theme = opts.theme || null;
76
+ this.keepAliveInterval = opts.keepAliveInterval || 15000;
77
+ this._pages = new Map();
78
+ this._clients = new Map();
79
+ this._server = null;
80
+ this._clientCounter = 0;
81
+ }
82
+
83
+ /**
84
+ * Register a page handler.
85
+ *
86
+ * @param {string} path - URL path (e.g., '/', '/dashboard')
87
+ * @param {Function} handler - Called with (client: BwServeClient) on connection
88
+ * @returns {BwServeApp} this (for chaining)
89
+ */
90
+ page(path, handler) {
91
+ this._pages.set(path, handler);
92
+ return this;
93
+ }
94
+
95
+ /**
96
+ * Start the HTTP server and begin accepting SSE connections.
97
+ *
98
+ * @param {Function} [callback] - Called when server is listening
99
+ * @returns {Promise<void>}
100
+ */
101
+ listen(callback) {
102
+ var self = this;
103
+
104
+ return new Promise(function(res) {
105
+ self._server = createServer(function(req, rawRes) {
106
+ self._handleRequest(req, rawRes);
107
+ });
108
+
109
+ self._server.listen(self.port, function() {
110
+ if (callback) callback();
111
+ res();
112
+ });
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Stop the server and close all client connections.
118
+ */
119
+ close() {
120
+ var self = this;
121
+ return new Promise(function(res) {
122
+ // Close all SSE streams
123
+ for (var record of self._clients.values()) {
124
+ if (record.client && typeof record.client.close === 'function') {
125
+ record.client.close();
126
+ }
127
+ }
128
+ self._clients.clear();
129
+
130
+ if (self._server) {
131
+ self._server.close(function() {
132
+ self._server = null;
133
+ res();
134
+ });
135
+ } else {
136
+ res();
137
+ }
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Get count of active client connections.
143
+ * @returns {number}
144
+ */
145
+ get clientCount() {
146
+ return this._clients.size;
147
+ }
148
+
149
+ /**
150
+ * Broadcast a protocol message to all connected clients.
151
+ *
152
+ * If msg has a clientId field, send only to that client.
153
+ * Otherwise, broadcast to all.
154
+ *
155
+ * @param {Object} msg - Protocol message (replace, patch, append, remove, batch)
156
+ * @returns {number} Number of clients that received the message
157
+ */
158
+ broadcast(msg) {
159
+ if (msg.clientId) {
160
+ var record = this._clients.get(msg.clientId);
161
+ if (record && record.client) {
162
+ record.client._send(msg);
163
+ return 1;
164
+ }
165
+ return 0;
166
+ }
167
+ var count = 0;
168
+ for (var record of this._clients.values()) {
169
+ if (record.client && !record.client._closed) {
170
+ record.client._send(msg);
171
+ count++;
172
+ }
173
+ }
174
+ return count;
175
+ }
176
+
177
+ /**
178
+ * Internal: route incoming HTTP requests.
179
+ * @private
180
+ */
181
+ _handleRequest(req, res) {
182
+ var url = req.url || '/';
183
+ var method = req.method || 'GET';
184
+
185
+ // Parse URL path (strip query string)
186
+ var path = url.split('?')[0];
187
+
188
+ // /__bw/bitwrench.umd.js — serve bitwrench client library
189
+ if (path === '/__bw/bitwrench.umd.js' && method === 'GET') {
190
+ return this._serveDistFile(res, 'bitwrench.umd.js');
191
+ }
192
+
193
+ // /__bw/bitwrench.umd.min.js — serve minified
194
+ if (path === '/__bw/bitwrench.umd.min.js' && method === 'GET') {
195
+ return this._serveDistFile(res, 'bitwrench.umd.min.js');
196
+ }
197
+
198
+ // /__bw/bitwrench.css — serve bitwrench CSS
199
+ if (path === '/__bw/bitwrench.css' && method === 'GET') {
200
+ return this._serveDistFile(res, 'bitwrench.css');
201
+ }
202
+
203
+ // /__bw/events/:clientId — SSE stream
204
+ if (path.startsWith('/__bw/events/') && method === 'GET') {
205
+ var clientId = path.slice('/__bw/events/'.length);
206
+ return this._handleSSE(req, res, clientId);
207
+ }
208
+
209
+ // /__bw/action/:clientId — action POST
210
+ if (path.startsWith('/__bw/action/') && method === 'POST') {
211
+ var actionClientId = path.slice('/__bw/action/'.length);
212
+ return this._handleAction(req, res, actionClientId);
213
+ }
214
+
215
+ // Registered page routes — serve shell HTML
216
+ if (method === 'GET' && this._pages.has(path)) {
217
+ var clientId2 = 'c' + (++this._clientCounter);
218
+ var shell = generateShell({
219
+ clientId: clientId2,
220
+ title: this.title,
221
+ theme: this.theme,
222
+ injectBitwrench: this.injectBitwrench
223
+ });
224
+ // Store the page path for this client so SSE knows which handler to call
225
+ this._clients.set(clientId2, { pagePath: path, client: null });
226
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
227
+ res.end(shell);
228
+ return;
229
+ }
230
+
231
+ // Static file serving
232
+ if (method === 'GET' && this.staticDir) {
233
+ var filePath = join(this.staticDir, path);
234
+ if (existsSync(filePath) && statSync(filePath).isFile()) {
235
+ var ext = extname(filePath);
236
+ var mime = MIME_TYPES[ext] || 'application/octet-stream';
237
+ var content = readFileSync(filePath);
238
+ res.writeHead(200, { 'Content-Type': mime });
239
+ res.end(content);
240
+ return;
241
+ }
242
+ }
243
+
244
+ // 404
245
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
246
+ res.end('Not Found');
247
+ }
248
+
249
+ /**
250
+ * Serve a file from the dist/ directory.
251
+ * @private
252
+ */
253
+ _serveDistFile(res, filename) {
254
+ var filePath = join(DIST_DIR, filename);
255
+ if (!existsSync(filePath)) {
256
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
257
+ res.end('Not Found: ' + filename);
258
+ return;
259
+ }
260
+ var ext = extname(filename);
261
+ var mime = MIME_TYPES[ext] || 'application/octet-stream';
262
+ var content = readFileSync(filePath);
263
+ res.writeHead(200, {
264
+ 'Content-Type': mime,
265
+ 'Cache-Control': 'public, max-age=3600'
266
+ });
267
+ res.end(content);
268
+ }
269
+
270
+ /**
271
+ * Handle an SSE connection.
272
+ * @private
273
+ */
274
+ _handleSSE(req, res, clientId) {
275
+ var self = this;
276
+
277
+ // Set SSE headers
278
+ res.writeHead(200, {
279
+ 'Content-Type': 'text/event-stream',
280
+ 'Cache-Control': 'no-cache',
281
+ 'Connection': 'keep-alive',
282
+ 'Access-Control-Allow-Origin': '*'
283
+ });
284
+
285
+ // Create client instance
286
+ var client = new BwServeClient(clientId, res);
287
+
288
+ // Look up the pending client record (set during page serve)
289
+ var pending = self._clients.get(clientId);
290
+ var pagePath = pending ? pending.pagePath : '/';
291
+ self._clients.set(clientId, { pagePath: pagePath, client: client });
292
+
293
+ // Keep-alive: send SSE comment periodically
294
+ var keepAlive = setInterval(function() {
295
+ if (!client._closed) {
296
+ try { res.write(':keepalive\n\n'); } catch (e) { /* ignore */ }
297
+ }
298
+ }, self.keepAliveInterval);
299
+
300
+ // Clean up on disconnect
301
+ req.on('close', function() {
302
+ clearInterval(keepAlive);
303
+ client._closed = true;
304
+ self._clients.delete(clientId);
305
+ });
306
+
307
+ // Call the page handler
308
+ var handler = self._pages.get(pagePath);
309
+ if (handler) {
310
+ try {
311
+ handler(client);
312
+ } catch (e) {
313
+ console.error('[bwserve] Page handler error:', e);
314
+ }
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Handle an action POST from a client.
320
+ * @private
321
+ */
322
+ _handleAction(req, res, clientId) {
323
+ var record = this._clients.get(clientId);
324
+ if (!record || !record.client) {
325
+ res.writeHead(404, { 'Content-Type': 'application/json' });
326
+ res.end(JSON.stringify({ error: 'Unknown client' }));
327
+ return;
328
+ }
329
+
330
+ var body = '';
331
+ req.on('data', function(chunk) {
332
+ body += chunk;
333
+ });
334
+ req.on('end', function() {
335
+ try {
336
+ var data = JSON.parse(body);
337
+ var action = data.action;
338
+ var payload = data.data || data;
339
+ record.client._dispatch(action, payload);
340
+ res.writeHead(200, { 'Content-Type': 'application/json' });
341
+ res.end(JSON.stringify({ ok: true }));
342
+ } catch (e) {
343
+ res.writeHead(400, { 'Content-Type': 'application/json' });
344
+ res.end(JSON.stringify({ error: e.message }));
345
+ }
346
+ });
347
+ }
348
+ }
349
+
350
+ export { BwServeApp, BwServeClient };
351
+
352
+ export default { create, BwServeApp, BwServeClient };
@@ -0,0 +1,103 @@
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
+ script.push(' onStatus: function(s) {');
68
+ script.push(' if (typeof console !== "undefined") console.log("[bwserve] " + s);');
69
+ script.push(' }');
70
+ script.push(' });');
71
+
72
+ // data-bw-action click delegation
73
+ script.push(' document.addEventListener("click", function(e) {');
74
+ script.push(' var el = e.target.closest ? e.target.closest("[data-bw-action]") : null;');
75
+ script.push(' if (!el) return;');
76
+ script.push(' e.preventDefault();');
77
+ script.push(' var actionData = {};');
78
+ script.push(' if (el.getAttribute("data-bw-id")) actionData.bwId = el.getAttribute("data-bw-id");');
79
+ script.push(' var form = el.closest("div") || document;');
80
+ script.push(' var inp = form.querySelector("input[type=text],input:not([type])");');
81
+ script.push(' if (inp) { actionData.inputValue = inp.value; inp.value = ""; }');
82
+ script.push(' conn.sendAction(el.getAttribute("data-bw-action"), actionData);');
83
+ script.push(' });');
84
+
85
+ // Enter key on inputs
86
+ script.push(' document.addEventListener("keydown", function(e) {');
87
+ script.push(' if (e.key === "Enter" && e.target.tagName === "INPUT") {');
88
+ script.push(' var form = e.target.closest("div") || document;');
89
+ script.push(' var btn = form.querySelector("[data-bw-action]");');
90
+ script.push(' if (btn) {');
91
+ script.push(' conn.sendAction(btn.getAttribute("data-bw-action"), { inputValue: e.target.value });');
92
+ script.push(' e.target.value = "";');
93
+ script.push(' }');
94
+ script.push(' }');
95
+ script.push(' });');
96
+
97
+ script.push('})();');
98
+ script.push('</script>');
99
+ script.push('</body>');
100
+ script.push('</html>');
101
+
102
+ return head.concat(script).join('\n');
103
+ }