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.
- package/README.md +127 -38
- package/dist/bitwrench-bccl.cjs.js +8 -8
- package/dist/bitwrench-bccl.cjs.min.js +3 -3
- package/dist/bitwrench-bccl.esm.js +8 -8
- package/dist/bitwrench-bccl.esm.min.js +3 -3
- package/dist/bitwrench-bccl.umd.js +8 -8
- package/dist/bitwrench-bccl.umd.min.js +2 -2
- package/dist/bitwrench-code-edit.cjs.js +1 -1
- package/dist/bitwrench-code-edit.cjs.min.js +1 -1
- package/dist/bitwrench-code-edit.es5.js +1 -1
- package/dist/bitwrench-code-edit.es5.min.js +1 -1
- package/dist/bitwrench-code-edit.esm.js +1 -1
- package/dist/bitwrench-code-edit.esm.min.js +1 -1
- package/dist/bitwrench-code-edit.umd.js +1 -1
- package/dist/bitwrench-code-edit.umd.min.js +1 -1
- package/dist/bitwrench-lean.cjs.js +941 -775
- package/dist/bitwrench-lean.cjs.min.js +20 -20
- package/dist/bitwrench-lean.es5.js +1012 -961
- package/dist/bitwrench-lean.es5.min.js +18 -18
- package/dist/bitwrench-lean.esm.js +941 -775
- package/dist/bitwrench-lean.esm.min.js +20 -20
- package/dist/bitwrench-lean.umd.js +941 -775
- package/dist/bitwrench-lean.umd.min.js +20 -20
- package/dist/bitwrench-util-css.cjs.js +236 -0
- package/dist/bitwrench-util-css.cjs.min.js +22 -0
- package/dist/bitwrench-util-css.es5.js +414 -0
- package/dist/bitwrench-util-css.es5.min.js +21 -0
- package/dist/bitwrench-util-css.esm.js +230 -0
- package/dist/bitwrench-util-css.esm.min.js +21 -0
- package/dist/bitwrench-util-css.umd.js +242 -0
- package/dist/bitwrench-util-css.umd.min.js +21 -0
- package/dist/bitwrench.cjs.js +948 -782
- package/dist/bitwrench.cjs.min.js +21 -21
- package/dist/bitwrench.css +456 -132
- package/dist/bitwrench.es5.js +1024 -970
- package/dist/bitwrench.es5.min.js +19 -19
- package/dist/bitwrench.esm.js +949 -783
- package/dist/bitwrench.esm.min.js +21 -21
- package/dist/bitwrench.min.css +1 -1
- package/dist/bitwrench.umd.js +948 -782
- package/dist/bitwrench.umd.min.js +21 -21
- package/dist/builds.json +178 -90
- package/dist/bwserve.cjs.js +514 -68
- package/dist/bwserve.esm.js +513 -69
- package/dist/sri.json +44 -36
- package/package.json +3 -2
- package/readme.html +136 -49
- package/src/bitwrench-bccl.js +7 -7
- package/src/bitwrench-color-utils.js +31 -9
- package/src/bitwrench-esm-entry.js +11 -0
- package/src/bitwrench-styles.js +439 -232
- package/src/bitwrench-util-css.js +229 -0
- package/src/bitwrench.js +483 -485
- package/src/bwserve/attach.js +57 -0
- package/src/bwserve/bwclient.js +141 -0
- package/src/bwserve/bwshell.js +102 -0
- package/src/bwserve/client.js +151 -1
- package/src/bwserve/index.js +127 -28
- package/src/cli/attach.js +555 -0
- package/src/cli/convert.js +2 -5
- package/src/cli/index.js +7 -0
- package/src/cli/inject.js +1 -1
- package/src/cli/serve.js +6 -2
- package/src/generate-css.js +11 -4
- package/src/vendor/html2canvas.min.js +20 -0
- package/src/version.js +3 -3
- package/src/bwserve/shell.js +0 -106
package/src/bitwrench.js
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { VERSION_INFO } from './version.js';
|
|
11
|
-
import { getStructuralStyles,
|
|
12
|
-
generateThemedCSS,
|
|
11
|
+
import { getStructuralStyles, getResetStyles,
|
|
12
|
+
generateThemedCSS, derivePalette as _derivePalette,
|
|
13
13
|
DEFAULT_PALETTE_CONFIG, SPACING_PRESETS, RADIUS_PRESETS, THEME_PRESETS,
|
|
14
14
|
TYPE_RATIO_PRESETS, ELEVATION_PRESETS, MOTION_PRESETS, generateTypeScale,
|
|
15
|
-
resolveLayout } from './bitwrench-styles.js';
|
|
15
|
+
resolveLayout, scopeRulesUnder } from './bitwrench-styles.js';
|
|
16
16
|
import { hexToHsl, hslToHex, adjustLightness, mixColor,
|
|
17
17
|
relativeLuminance, textOnColor, deriveShades,
|
|
18
18
|
derivePalette, harmonize, deriveAlternateSeed, deriveAlternateConfig,
|
|
@@ -364,7 +364,12 @@ bw._el = function(id) {
|
|
|
364
364
|
el = document.querySelector('[data-bw_id="' + id + '"]');
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
// 5.
|
|
367
|
+
// 5. Try class-based lookup for bw_uuid_* tokens (UUID addressing)
|
|
368
|
+
if (!el && id.indexOf('bw_uuid_') === 0) {
|
|
369
|
+
el = document.querySelector('.' + id);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 6. Cache the result for next time
|
|
368
373
|
if (el) {
|
|
369
374
|
bw._nodeMap[id] = el;
|
|
370
375
|
}
|
|
@@ -417,6 +422,84 @@ bw._deregisterNode = function(el, bwId) {
|
|
|
417
422
|
}
|
|
418
423
|
};
|
|
419
424
|
|
|
425
|
+
// ===================================================================================
|
|
426
|
+
// bw.assignUUID() / bw.getUUID() — Explicit UUID addressing for TACO objects
|
|
427
|
+
// ===================================================================================
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Regex to match a bw_uuid_* token in a class string.
|
|
431
|
+
* @private
|
|
432
|
+
*/
|
|
433
|
+
var _UUID_RE = /\bbw_uuid_[a-z0-9_]+\b/;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Assign a UUID to a TACO object by appending a `bw_uuid_*` token to `taco.a.class`.
|
|
437
|
+
*
|
|
438
|
+
* Idempotent by default — calling twice returns the same UUID. Pass `forceNew=true`
|
|
439
|
+
* to replace an existing UUID (useful in loops where each TACO needs a unique ID).
|
|
440
|
+
*
|
|
441
|
+
* @param {Object} taco - A TACO object `{t, a, c, o}`
|
|
442
|
+
* @param {boolean} [forceNew=false] - If true, replaces any existing UUID with a new one
|
|
443
|
+
* @returns {string} The UUID string (e.g. 'bw_uuid_a1b2c3d4e5')
|
|
444
|
+
* @category Identifiers
|
|
445
|
+
* @example
|
|
446
|
+
* var card = bw.makeStatCard({ value: '0', label: 'Scans' });
|
|
447
|
+
* var uuid = bw.assignUUID(card); // 'bw_uuid_a1b2c3d4e5'
|
|
448
|
+
* var same = bw.assignUUID(card); // same UUID (idempotent)
|
|
449
|
+
* var diff = bw.assignUUID(card, true); // new UUID (forced)
|
|
450
|
+
*/
|
|
451
|
+
bw.assignUUID = function(taco, forceNew) {
|
|
452
|
+
if (!taco || !_is(taco, 'object')) return null;
|
|
453
|
+
|
|
454
|
+
// Ensure taco.a exists
|
|
455
|
+
if (!taco.a) taco.a = {};
|
|
456
|
+
if (!_is(taco.a.class, 'string')) taco.a.class = taco.a.class ? String(taco.a.class) : '';
|
|
457
|
+
|
|
458
|
+
var existing = taco.a.class.match(_UUID_RE);
|
|
459
|
+
|
|
460
|
+
if (existing && !forceNew) {
|
|
461
|
+
return existing[0];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Remove old UUID if forceNew
|
|
465
|
+
if (existing) {
|
|
466
|
+
taco.a.class = taco.a.class.replace(_UUID_RE, '').replace(/\s+/g, ' ').trim();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
var uuid = bw.uuid('uuid');
|
|
470
|
+
taco.a.class = (taco.a.class ? taco.a.class + ' ' : '') + uuid;
|
|
471
|
+
return uuid;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Read the UUID from a TACO object or DOM element. Pure getter, no side effects.
|
|
476
|
+
*
|
|
477
|
+
* @param {Object|Element} tacoOrElement - A TACO object or DOM element
|
|
478
|
+
* @returns {string|null} The UUID string, or null if none assigned
|
|
479
|
+
* @category Identifiers
|
|
480
|
+
* @example
|
|
481
|
+
* bw.getUUID(card) // 'bw_uuid_a1b2c3d4e5' (from TACO)
|
|
482
|
+
* bw.getUUID(domEl) // 'bw_uuid_a1b2c3d4e5' (from DOM element)
|
|
483
|
+
* bw.getUUID({t:'div'}) // null (no UUID)
|
|
484
|
+
*/
|
|
485
|
+
bw.getUUID = function(tacoOrElement) {
|
|
486
|
+
if (!tacoOrElement) return null;
|
|
487
|
+
|
|
488
|
+
var classStr;
|
|
489
|
+
// DOM element: check className
|
|
490
|
+
if (tacoOrElement.className !== undefined && tacoOrElement.tagName) {
|
|
491
|
+
classStr = tacoOrElement.className;
|
|
492
|
+
}
|
|
493
|
+
// TACO object: check a.class
|
|
494
|
+
else if (tacoOrElement.a && _is(tacoOrElement.a.class, 'string')) {
|
|
495
|
+
classStr = tacoOrElement.a.class;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!classStr) return null;
|
|
499
|
+
var match = classStr.match(_UUID_RE);
|
|
500
|
+
return match ? match[0] : null;
|
|
501
|
+
};
|
|
502
|
+
|
|
420
503
|
/**
|
|
421
504
|
* Escape HTML special characters to prevent XSS.
|
|
422
505
|
*
|
|
@@ -466,6 +549,42 @@ bw.raw = function(str) {
|
|
|
466
549
|
return { __bw_raw: true, v: String(str) };
|
|
467
550
|
};
|
|
468
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Hyperscript-style TACO constructor.
|
|
554
|
+
*
|
|
555
|
+
* A convenience helper that returns a canonical TACO object from positional
|
|
556
|
+
* arguments. The return value is a plain object — serializable, works with
|
|
557
|
+
* bwserve, and accepted everywhere TACO is accepted.
|
|
558
|
+
*
|
|
559
|
+
* @param {string} tag - HTML tag name (e.g. 'div', 'p', 'section')
|
|
560
|
+
* @param {Object|null} [attrs] - HTML attributes object. Pass null or omit to skip.
|
|
561
|
+
* @param {*} [content] - Content: string, number, TACO object, or array of children.
|
|
562
|
+
* @param {Object} [options] - TACO options (state, lifecycle hooks, render fn).
|
|
563
|
+
* @returns {Object} Plain TACO object {t, a?, c?, o?}
|
|
564
|
+
* @category Utilities
|
|
565
|
+
* @see bw.html
|
|
566
|
+
* @see bw.createDOM
|
|
567
|
+
* @see bw.DOM
|
|
568
|
+
* @example
|
|
569
|
+
* bw.h('div')
|
|
570
|
+
* // => { t: 'div' }
|
|
571
|
+
*
|
|
572
|
+
* bw.h('p', { class: 'bw_text_muted' }, 'Hello')
|
|
573
|
+
* // => { t: 'p', a: { class: 'bw_text_muted' }, c: 'Hello' }
|
|
574
|
+
*
|
|
575
|
+
* bw.h('ul', null, [
|
|
576
|
+
* bw.h('li', null, 'one'),
|
|
577
|
+
* bw.h('li', null, 'two')
|
|
578
|
+
* ])
|
|
579
|
+
* // => { t: 'ul', c: [{ t: 'li', c: 'one' }, { t: 'li', c: 'two' }] }
|
|
580
|
+
*/
|
|
581
|
+
bw.h = function(tag, attrs, content, options) {
|
|
582
|
+
var taco = { t: String(tag) };
|
|
583
|
+
if (attrs !== null && attrs !== undefined) taco.a = attrs;
|
|
584
|
+
if (content !== undefined) taco.c = content;
|
|
585
|
+
if (options !== undefined) taco.o = options;
|
|
586
|
+
return taco;
|
|
587
|
+
};
|
|
469
588
|
|
|
470
589
|
/**
|
|
471
590
|
* Convert a TACO object (or array of TACOs) to an HTML string.
|
|
@@ -730,7 +849,7 @@ bw.htmlPage = function(opts) {
|
|
|
730
849
|
? (THEME_PRESETS[theme.toLowerCase()] || null)
|
|
731
850
|
: theme;
|
|
732
851
|
if (themeConfig) {
|
|
733
|
-
var themeResult = bw.
|
|
852
|
+
var themeResult = bw.makeStyles(themeConfig);
|
|
734
853
|
themeCSS = themeResult.css;
|
|
735
854
|
}
|
|
736
855
|
}
|
|
@@ -756,14 +875,14 @@ bw.htmlPage = function(opts) {
|
|
|
756
875
|
// Combine all CSS
|
|
757
876
|
var allCSS = (themeCSS ? themeCSS + '\n' : '') + css;
|
|
758
877
|
|
|
759
|
-
// Body-end script: registry entries + optional
|
|
878
|
+
// Body-end script: registry entries + optional loadStyles
|
|
760
879
|
var bodyEndScript = '';
|
|
761
880
|
var bodyEndParts = [];
|
|
762
881
|
if (registryEntries) {
|
|
763
882
|
bodyEndParts.push(registryEntries);
|
|
764
883
|
}
|
|
765
884
|
if (runtime === 'inline' || runtime === 'cdn') {
|
|
766
|
-
bodyEndParts.push('if(typeof bw!=="undefined"){bw.
|
|
885
|
+
bodyEndParts.push('if(typeof bw!=="undefined"){bw.loadStyles();}');
|
|
767
886
|
}
|
|
768
887
|
if (bodyEndParts.length > 0) {
|
|
769
888
|
bodyEndScript = '<script>\n' + bodyEndParts.join('\n') + '\n</script>';
|
|
@@ -937,6 +1056,14 @@ bw.createDOM = function(taco, options = {}) {
|
|
|
937
1056
|
bw._registerNode(el, null);
|
|
938
1057
|
}
|
|
939
1058
|
|
|
1059
|
+
// Register UUID class in node cache (bw_uuid_* tokens in class string)
|
|
1060
|
+
if (el.className) {
|
|
1061
|
+
var uuidMatch = el.className.match(_UUID_RE);
|
|
1062
|
+
if (uuidMatch) {
|
|
1063
|
+
bw._nodeMap[uuidMatch[0]] = el;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
940
1067
|
// Handle lifecycle hooks and state
|
|
941
1068
|
if (opts.mounted || opts.unmount || opts.render || opts.state) {
|
|
942
1069
|
const id = attrs['data-bw_id'] || bw.uuid();
|
|
@@ -1309,6 +1436,16 @@ bw.renderComponent = function(taco, options = {}) {
|
|
|
1309
1436
|
bw.cleanup = function(element) {
|
|
1310
1437
|
if (!bw._isBrowser || !element) return;
|
|
1311
1438
|
|
|
1439
|
+
// Deregister UUID classes from node cache (element + descendants)
|
|
1440
|
+
// Covers elements that have UUID but no data-bw_id
|
|
1441
|
+
var selfUuidMatch = element.className && element.className.match(_UUID_RE);
|
|
1442
|
+
if (selfUuidMatch) delete bw._nodeMap[selfUuidMatch[0]];
|
|
1443
|
+
var uuidEls = element.querySelectorAll('[class*="bw_uuid_"]');
|
|
1444
|
+
uuidEls.forEach(function(uel) {
|
|
1445
|
+
var m = uel.className && uel.className.match(_UUID_RE);
|
|
1446
|
+
if (m) delete bw._nodeMap[m[0]];
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1312
1449
|
// Find all elements with data-bw_id
|
|
1313
1450
|
const elements = element.querySelectorAll('[data-bw_id]');
|
|
1314
1451
|
|
|
@@ -1324,6 +1461,10 @@ bw.cleanup = function(element) {
|
|
|
1324
1461
|
// Deregister from node cache
|
|
1325
1462
|
bw._deregisterNode(el, id);
|
|
1326
1463
|
|
|
1464
|
+
// Deregister UUID class from node cache
|
|
1465
|
+
var uuidMatch = el.className && el.className.match(_UUID_RE);
|
|
1466
|
+
if (uuidMatch) delete bw._nodeMap[uuidMatch[0]];
|
|
1467
|
+
|
|
1327
1468
|
// Clean up pub/sub subscriptions tied to this element
|
|
1328
1469
|
if (el._bw_subs) {
|
|
1329
1470
|
el._bw_subs.forEach(function(unsub) { unsub(); });
|
|
@@ -1348,6 +1489,10 @@ bw.cleanup = function(element) {
|
|
|
1348
1489
|
// Deregister from node cache
|
|
1349
1490
|
bw._deregisterNode(element, id);
|
|
1350
1491
|
|
|
1492
|
+
// Deregister UUID class from node cache
|
|
1493
|
+
var elemUuidMatch = element.className && element.className.match(_UUID_RE);
|
|
1494
|
+
if (elemUuidMatch) delete bw._nodeMap[elemUuidMatch[0]];
|
|
1495
|
+
|
|
1351
1496
|
// Clean up pub/sub subscriptions tied to element itself
|
|
1352
1497
|
if (element._bw_subs) {
|
|
1353
1498
|
element._bw_subs.forEach(function(unsub) { unsub(); });
|
|
@@ -1959,7 +2104,7 @@ function ComponentHandle(taco) {
|
|
|
1959
2104
|
willMount: o.willMount || null,
|
|
1960
2105
|
mounted: o.mounted || null,
|
|
1961
2106
|
willUpdate: o.willUpdate || null,
|
|
1962
|
-
onUpdate: o.onUpdate || null,
|
|
2107
|
+
onUpdate: o.onUpdate || o.updated || null,
|
|
1963
2108
|
unmount: o.unmount || null,
|
|
1964
2109
|
willDestroy: o.willDestroy || null
|
|
1965
2110
|
};
|
|
@@ -2898,7 +3043,7 @@ bw.component = function(taco) {
|
|
|
2898
3043
|
* and calls the named method. This is the bitwrench equivalent of
|
|
2899
3044
|
* Win32 SendMessage(hwnd, msg, wParam, lParam).
|
|
2900
3045
|
*
|
|
2901
|
-
* @param {string} target - Component UUID (data-bw_comp_id) or user tag (CSS class)
|
|
3046
|
+
* @param {string} target - Component UUID (bw_uuid_*), comp ID (data-bw_comp_id), or user tag (CSS class)
|
|
2902
3047
|
* @param {string} action - Method name to call on the component
|
|
2903
3048
|
* @param {*} data - Data to pass to the method
|
|
2904
3049
|
* @returns {boolean} True if message was dispatched successfully
|
|
@@ -2915,9 +3060,14 @@ bw.component = function(taco) {
|
|
|
2915
3060
|
* };
|
|
2916
3061
|
*/
|
|
2917
3062
|
bw.message = function(target, action, data) {
|
|
2918
|
-
// Try
|
|
2919
|
-
var el = bw
|
|
2920
|
-
|
|
3063
|
+
// Try bw._el() first (handles UUID class, nodeMap cache, getElementById)
|
|
3064
|
+
var el = bw._el(target);
|
|
3065
|
+
// Then try data-bw_comp_id attribute
|
|
3066
|
+
if (!el || !el._bwComponentHandle) {
|
|
3067
|
+
el = bw.$('[data-bw_comp_id="' + target + '"]')[0];
|
|
3068
|
+
}
|
|
3069
|
+
// Then try CSS class (user tag)
|
|
3070
|
+
if (!el || !el._bwComponentHandle) {
|
|
2921
3071
|
el = bw.$('.' + target)[0];
|
|
2922
3072
|
}
|
|
2923
3073
|
if (!el || !el._bwComponentHandle) return false;
|
|
@@ -2931,59 +3081,24 @@ bw.message = function(target, action, data) {
|
|
|
2931
3081
|
};
|
|
2932
3082
|
|
|
2933
3083
|
// ===================================================================================
|
|
2934
|
-
// bw.
|
|
3084
|
+
// bw.apply() / bw.parseJSONFlex() — Server-driven UI protocol
|
|
2935
3085
|
// ===================================================================================
|
|
2936
3086
|
|
|
2937
3087
|
/**
|
|
2938
3088
|
* Registry of named functions sent via register messages.
|
|
2939
|
-
* Populated by
|
|
2940
|
-
* Invoked by
|
|
3089
|
+
* Populated by bw.apply({ type: 'register', name, body }).
|
|
3090
|
+
* Invoked by bw.apply({ type: 'call', name, args }).
|
|
2941
3091
|
* @private
|
|
2942
3092
|
*/
|
|
2943
3093
|
bw._clientFunctions = {};
|
|
2944
3094
|
|
|
2945
3095
|
/**
|
|
2946
|
-
* Whether exec messages are allowed. Set by
|
|
3096
|
+
* Whether exec messages are allowed. Set by bwclient connect opts.allowExec.
|
|
2947
3097
|
* Default false — exec messages are rejected unless explicitly opted in.
|
|
2948
3098
|
* @private
|
|
2949
3099
|
*/
|
|
2950
3100
|
bw._allowExec = false;
|
|
2951
3101
|
|
|
2952
|
-
/**
|
|
2953
|
-
* Built-in client functions available via call() without registration.
|
|
2954
|
-
* @private
|
|
2955
|
-
*/
|
|
2956
|
-
bw._builtinClientFunctions = {
|
|
2957
|
-
scrollTo: function(selector) {
|
|
2958
|
-
var el = bw._el(selector);
|
|
2959
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
2960
|
-
},
|
|
2961
|
-
focus: function(selector) {
|
|
2962
|
-
var el = bw._el(selector);
|
|
2963
|
-
if (el && _is(el.focus, 'function')) el.focus();
|
|
2964
|
-
},
|
|
2965
|
-
download: function(filename, content, mimeType) {
|
|
2966
|
-
if (typeof document === 'undefined') return;
|
|
2967
|
-
var blob = new Blob([content], { type: mimeType || 'text/plain' });
|
|
2968
|
-
var a = document.createElement('a');
|
|
2969
|
-
a.href = URL.createObjectURL(blob);
|
|
2970
|
-
a.download = filename;
|
|
2971
|
-
a.click();
|
|
2972
|
-
URL.revokeObjectURL(a.href);
|
|
2973
|
-
},
|
|
2974
|
-
clipboard: function(text) {
|
|
2975
|
-
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
2976
|
-
navigator.clipboard.writeText(text);
|
|
2977
|
-
}
|
|
2978
|
-
},
|
|
2979
|
-
redirect: function(url) {
|
|
2980
|
-
if (typeof window !== 'undefined') window.location.href = url;
|
|
2981
|
-
},
|
|
2982
|
-
log: function() {
|
|
2983
|
-
console.log.apply(console, arguments);
|
|
2984
|
-
}
|
|
2985
|
-
};
|
|
2986
|
-
|
|
2987
3102
|
/**
|
|
2988
3103
|
* Parse a bwserve protocol message string, supporting both strict JSON
|
|
2989
3104
|
* and r-prefixed relaxed JSON (single-quoted strings, trailing commas).
|
|
@@ -2998,9 +3113,9 @@ bw._builtinClientFunctions = {
|
|
|
2998
3113
|
* @param {string} str - JSON or r-prefixed relaxed JSON string
|
|
2999
3114
|
* @returns {Object} Parsed message object
|
|
3000
3115
|
* @throws {SyntaxError} If the string is not valid JSON or relaxed JSON
|
|
3001
|
-
* @category
|
|
3116
|
+
* @category Core
|
|
3002
3117
|
*/
|
|
3003
|
-
bw.
|
|
3118
|
+
bw.parseJSONFlex = function(str) {
|
|
3004
3119
|
str = (str || '').trim();
|
|
3005
3120
|
if (str.charAt(0) !== 'r') return JSON.parse(str);
|
|
3006
3121
|
str = str.slice(1);
|
|
@@ -3085,10 +3200,10 @@ bw.clientParse = function(str) {
|
|
|
3085
3200
|
* append — target.appendChild(bw.createDOM(node))
|
|
3086
3201
|
* remove — bw.cleanup(target); target.remove()
|
|
3087
3202
|
* patch — bw.patch(target, content, attr)
|
|
3088
|
-
* batch — iterate ops, call
|
|
3203
|
+
* batch — iterate ops, call bw.apply for each
|
|
3089
3204
|
* message — bw.message(target, action, data)
|
|
3090
3205
|
* register — store a named function for later call()
|
|
3091
|
-
* call — invoke a registered
|
|
3206
|
+
* call — invoke a registered function
|
|
3092
3207
|
* exec — execute arbitrary JS (requires allowExec)
|
|
3093
3208
|
*
|
|
3094
3209
|
* Target resolution:
|
|
@@ -3097,9 +3212,9 @@ bw.clientParse = function(str) {
|
|
|
3097
3212
|
*
|
|
3098
3213
|
* @param {Object} msg - Protocol message
|
|
3099
3214
|
* @returns {boolean} true if the message was applied successfully
|
|
3100
|
-
* @category
|
|
3215
|
+
* @category Core
|
|
3101
3216
|
*/
|
|
3102
|
-
bw.
|
|
3217
|
+
bw.apply = function(msg) {
|
|
3103
3218
|
if (!msg || !msg.type) return false;
|
|
3104
3219
|
|
|
3105
3220
|
var type = msg.type;
|
|
@@ -3133,7 +3248,7 @@ bw.clientApply = function(msg) {
|
|
|
3133
3248
|
if (!_isA(msg.ops)) return false;
|
|
3134
3249
|
var allOk = true;
|
|
3135
3250
|
msg.ops.forEach(function(op) {
|
|
3136
|
-
if (!bw.
|
|
3251
|
+
if (!bw.apply(op)) allOk = false;
|
|
3137
3252
|
});
|
|
3138
3253
|
return allOk;
|
|
3139
3254
|
|
|
@@ -3152,7 +3267,7 @@ bw.clientApply = function(msg) {
|
|
|
3152
3267
|
|
|
3153
3268
|
} else if (type === 'call') {
|
|
3154
3269
|
if (!msg.name) return false;
|
|
3155
|
-
var fn = bw._clientFunctions[msg.name]
|
|
3270
|
+
var fn = bw._clientFunctions[msg.name];
|
|
3156
3271
|
if (!_is(fn, 'function')) return false;
|
|
3157
3272
|
try {
|
|
3158
3273
|
var args = _isA(msg.args) ? msg.args : [];
|
|
@@ -3181,139 +3296,6 @@ bw.clientApply = function(msg) {
|
|
|
3181
3296
|
return false;
|
|
3182
3297
|
};
|
|
3183
3298
|
|
|
3184
|
-
/**
|
|
3185
|
-
* Connect to a bwserve SSE endpoint and apply protocol messages automatically.
|
|
3186
|
-
*
|
|
3187
|
-
* Returns a connection object with sendAction(), on(), and close() methods.
|
|
3188
|
-
*
|
|
3189
|
-
* @param {string} url - SSE endpoint URL (e.g., '/__bw/events/client-1')
|
|
3190
|
-
* @param {Object} [opts] - Connection options
|
|
3191
|
-
* @param {string} [opts.transport='sse'] - Transport type: 'sse' (default) or 'poll'
|
|
3192
|
-
* @param {number} [opts.interval=2000] - Poll interval in ms (only for 'poll' transport)
|
|
3193
|
-
* @param {string} [opts.actionUrl] - POST endpoint for actions (default: derived from url)
|
|
3194
|
-
* @param {boolean} [opts.reconnect=true] - Auto-reconnect on disconnect
|
|
3195
|
-
* @param {boolean} [opts.allowExec=false] - Enable exec message type (arbitrary JS execution)
|
|
3196
|
-
* @param {Function} [opts.onStatus] - Status callback: 'connecting'|'connected'|'disconnected'
|
|
3197
|
-
* @param {Function} [opts.onMessage] - Raw message callback (before clientApply)
|
|
3198
|
-
* @returns {Object} Connection object { sendAction, on, close, status }
|
|
3199
|
-
* @category Server
|
|
3200
|
-
*/
|
|
3201
|
-
bw.clientConnect = function(url, opts) {
|
|
3202
|
-
opts = opts || {};
|
|
3203
|
-
var transport = opts.transport || 'sse';
|
|
3204
|
-
var actionUrl = opts.actionUrl || url.replace(/\/events\//, '/action/');
|
|
3205
|
-
var reconnect = opts.reconnect !== false;
|
|
3206
|
-
var onStatus = opts.onStatus || function() {};
|
|
3207
|
-
var onMessage = opts.onMessage || null;
|
|
3208
|
-
var handlers = {};
|
|
3209
|
-
// Set the global allowExec flag from connection options
|
|
3210
|
-
bw._allowExec = !!opts.allowExec;
|
|
3211
|
-
var conn = {
|
|
3212
|
-
status: 'connecting',
|
|
3213
|
-
_es: null,
|
|
3214
|
-
_pollTimer: null
|
|
3215
|
-
};
|
|
3216
|
-
|
|
3217
|
-
function setStatus(s) {
|
|
3218
|
-
conn.status = s;
|
|
3219
|
-
onStatus(s);
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
function handleMessage(data) {
|
|
3223
|
-
try {
|
|
3224
|
-
var msg = _is(data, 'string') ? bw.clientParse(data) : data;
|
|
3225
|
-
if (onMessage) onMessage(msg);
|
|
3226
|
-
if (handlers.message) handlers.message(msg);
|
|
3227
|
-
bw.clientApply(msg);
|
|
3228
|
-
} catch (e) {
|
|
3229
|
-
if (handlers.error) handlers.error(e);
|
|
3230
|
-
}
|
|
3231
|
-
}
|
|
3232
|
-
|
|
3233
|
-
if (transport === 'sse' && typeof EventSource !== 'undefined') {
|
|
3234
|
-
setStatus('connecting');
|
|
3235
|
-
var es = new EventSource(url);
|
|
3236
|
-
conn._es = es;
|
|
3237
|
-
|
|
3238
|
-
es.onopen = function() {
|
|
3239
|
-
setStatus('connected');
|
|
3240
|
-
if (handlers.open) handlers.open();
|
|
3241
|
-
};
|
|
3242
|
-
|
|
3243
|
-
es.onmessage = function(e) {
|
|
3244
|
-
handleMessage(e.data);
|
|
3245
|
-
};
|
|
3246
|
-
|
|
3247
|
-
es.onerror = function() {
|
|
3248
|
-
if (conn.status === 'connected') {
|
|
3249
|
-
setStatus('disconnected');
|
|
3250
|
-
}
|
|
3251
|
-
if (handlers.error) handlers.error(new Error('SSE connection error'));
|
|
3252
|
-
if (!reconnect) {
|
|
3253
|
-
es.close();
|
|
3254
|
-
}
|
|
3255
|
-
// EventSource auto-reconnects by default when reconnect=true
|
|
3256
|
-
};
|
|
3257
|
-
} else if (transport === 'poll') {
|
|
3258
|
-
var interval = opts.interval || 2000;
|
|
3259
|
-
setStatus('connected');
|
|
3260
|
-
conn._pollTimer = setInterval(function() {
|
|
3261
|
-
fetch(url).then(function(r) { return r.json(); }).then(function(msgs) {
|
|
3262
|
-
if (_isA(msgs)) {
|
|
3263
|
-
msgs.forEach(handleMessage);
|
|
3264
|
-
} else if (msgs && msgs.type) {
|
|
3265
|
-
handleMessage(msgs);
|
|
3266
|
-
}
|
|
3267
|
-
}).catch(function(e) {
|
|
3268
|
-
if (handlers.error) handlers.error(e);
|
|
3269
|
-
});
|
|
3270
|
-
}, interval);
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
/**
|
|
3274
|
-
* Send an action to the server via POST.
|
|
3275
|
-
* @param {string} action - Action name
|
|
3276
|
-
* @param {Object} [data] - Action payload
|
|
3277
|
-
*/
|
|
3278
|
-
conn.sendAction = function(action, data) {
|
|
3279
|
-
var body = JSON.stringify({ type: 'action', action: action, data: data || {} });
|
|
3280
|
-
fetch(actionUrl, {
|
|
3281
|
-
method: 'POST',
|
|
3282
|
-
headers: { 'Content-Type': 'application/json' },
|
|
3283
|
-
body: body
|
|
3284
|
-
}).catch(function(e) {
|
|
3285
|
-
if (handlers.error) handlers.error(e);
|
|
3286
|
-
});
|
|
3287
|
-
};
|
|
3288
|
-
|
|
3289
|
-
/**
|
|
3290
|
-
* Register an event handler.
|
|
3291
|
-
* @param {string} event - 'open'|'message'|'error'|'close'
|
|
3292
|
-
* @param {Function} handler
|
|
3293
|
-
*/
|
|
3294
|
-
conn.on = function(event, handler) {
|
|
3295
|
-
handlers[event] = handler;
|
|
3296
|
-
return conn;
|
|
3297
|
-
};
|
|
3298
|
-
|
|
3299
|
-
/**
|
|
3300
|
-
* Close the connection.
|
|
3301
|
-
*/
|
|
3302
|
-
conn.close = function() {
|
|
3303
|
-
if (conn._es) {
|
|
3304
|
-
conn._es.close();
|
|
3305
|
-
conn._es = null;
|
|
3306
|
-
}
|
|
3307
|
-
if (conn._pollTimer) {
|
|
3308
|
-
clearInterval(conn._pollTimer);
|
|
3309
|
-
conn._pollTimer = null;
|
|
3310
|
-
}
|
|
3311
|
-
setStatus('disconnected');
|
|
3312
|
-
if (handlers.close) handlers.close();
|
|
3313
|
-
};
|
|
3314
|
-
|
|
3315
|
-
return conn;
|
|
3316
|
-
};
|
|
3317
3299
|
|
|
3318
3300
|
// ===================================================================================
|
|
3319
3301
|
// bw.inspect() — Debug utility
|
|
@@ -3522,7 +3504,7 @@ bw.css = function(rules, options = {}) {
|
|
|
3522
3504
|
* @returns {Element} The style element
|
|
3523
3505
|
* @category CSS & Styling
|
|
3524
3506
|
* @see bw.css
|
|
3525
|
-
* @see bw.
|
|
3507
|
+
* @see bw.loadStyles
|
|
3526
3508
|
* @example
|
|
3527
3509
|
* bw.injectCSS('.my-class { color: red; }');
|
|
3528
3510
|
* bw.injectCSS({ '.card': { padding: '1rem' } }, { id: 'card-styles' });
|
|
@@ -3567,9 +3549,8 @@ bw.injectCSS = function(css, options = {}) {
|
|
|
3567
3549
|
* @param {...Object} styles - Style objects to merge (left-to-right)
|
|
3568
3550
|
* @returns {Object} Merged style object
|
|
3569
3551
|
* @category CSS & Styling
|
|
3570
|
-
* @see bw.u
|
|
3571
3552
|
* @example
|
|
3572
|
-
* var style = bw.s(
|
|
3553
|
+
* var style = bw.s({ display: 'flex' }, { gap: '1rem' }, { color: 'red' });
|
|
3573
3554
|
* // => { display: 'flex', gap: '1rem', color: 'red' }
|
|
3574
3555
|
*/
|
|
3575
3556
|
bw.s = function() {
|
|
@@ -3581,99 +3562,6 @@ bw.s = function() {
|
|
|
3581
3562
|
return result;
|
|
3582
3563
|
};
|
|
3583
3564
|
|
|
3584
|
-
/**
|
|
3585
|
-
* Pre-built CSS utility objects (like Tailwind utilities, but in JS).
|
|
3586
|
-
*
|
|
3587
|
-
* Compose with `bw.s()` to build inline styles without writing raw CSS strings.
|
|
3588
|
-
* Includes flex, padding, margin, typography, color, border, and transition utilities.
|
|
3589
|
-
*
|
|
3590
|
-
* @category CSS & Styling
|
|
3591
|
-
* @see bw.s
|
|
3592
|
-
* @example
|
|
3593
|
-
* { t: 'div', a: { style: bw.s(bw.u.flex, bw.u.gap4, bw.u.p4) },
|
|
3594
|
-
* c: 'Flexbox with 1rem gap and padding' }
|
|
3595
|
-
*/
|
|
3596
|
-
bw.u = {
|
|
3597
|
-
// Display
|
|
3598
|
-
flex: { display: 'flex' },
|
|
3599
|
-
flexCol: { display: 'flex', flexDirection: 'column' },
|
|
3600
|
-
flexRow: { display: 'flex', flexDirection: 'row' },
|
|
3601
|
-
flexWrap: { display: 'flex', flexWrap: 'wrap' },
|
|
3602
|
-
block: { display: 'block' },
|
|
3603
|
-
inline: { display: 'inline' },
|
|
3604
|
-
hidden: { display: 'none' },
|
|
3605
|
-
|
|
3606
|
-
// Flex alignment
|
|
3607
|
-
justifyCenter: { justifyContent: 'center' },
|
|
3608
|
-
justifyBetween: { justifyContent: 'space-between' },
|
|
3609
|
-
justifyEnd: { justifyContent: 'flex-end' },
|
|
3610
|
-
alignCenter: { alignItems: 'center' },
|
|
3611
|
-
alignStart: { alignItems: 'flex-start' },
|
|
3612
|
-
alignEnd: { alignItems: 'flex-end' },
|
|
3613
|
-
|
|
3614
|
-
// Gap (0.25rem increments)
|
|
3615
|
-
gap1: { gap: '0.25rem' },
|
|
3616
|
-
gap2: { gap: '0.5rem' },
|
|
3617
|
-
gap3: { gap: '0.75rem' },
|
|
3618
|
-
gap4: { gap: '1rem' },
|
|
3619
|
-
gap6: { gap: '1.5rem' },
|
|
3620
|
-
gap8: { gap: '2rem' },
|
|
3621
|
-
|
|
3622
|
-
// Padding
|
|
3623
|
-
p0: { padding: '0' },
|
|
3624
|
-
p1: { padding: '0.25rem' },
|
|
3625
|
-
p2: { padding: '0.5rem' },
|
|
3626
|
-
p3: { padding: '0.75rem' },
|
|
3627
|
-
p4: { padding: '1rem' },
|
|
3628
|
-
p6: { padding: '1.5rem' },
|
|
3629
|
-
p8: { padding: '2rem' },
|
|
3630
|
-
px4: { paddingLeft: '1rem', paddingRight: '1rem' },
|
|
3631
|
-
py2: { paddingTop: '0.5rem', paddingBottom: '0.5rem' },
|
|
3632
|
-
py4: { paddingTop: '1rem', paddingBottom: '1rem' },
|
|
3633
|
-
|
|
3634
|
-
// Margin (same scale)
|
|
3635
|
-
m0: { margin: '0' },
|
|
3636
|
-
m4: { margin: '1rem' },
|
|
3637
|
-
mt2: { marginTop: '0.5rem' },
|
|
3638
|
-
mt4: { marginTop: '1rem' },
|
|
3639
|
-
mb2: { marginBottom: '0.5rem' },
|
|
3640
|
-
mb4: { marginBottom: '1rem' },
|
|
3641
|
-
mx_auto: { marginLeft: 'auto', marginRight: 'auto' },
|
|
3642
|
-
|
|
3643
|
-
// Typography
|
|
3644
|
-
textSm: { fontSize: '0.875rem' },
|
|
3645
|
-
textBase: { fontSize: '1rem' },
|
|
3646
|
-
textLg: { fontSize: '1.125rem' },
|
|
3647
|
-
textXl: { fontSize: '1.25rem' },
|
|
3648
|
-
text2xl: { fontSize: '1.5rem' },
|
|
3649
|
-
text3xl: { fontSize: '1.875rem' },
|
|
3650
|
-
bold: { fontWeight: '700' },
|
|
3651
|
-
semibold: { fontWeight: '600' },
|
|
3652
|
-
italic: { fontStyle: 'italic' },
|
|
3653
|
-
textCenter: { textAlign: 'center' },
|
|
3654
|
-
textRight: { textAlign: 'right' },
|
|
3655
|
-
|
|
3656
|
-
// Colors (from design tokens)
|
|
3657
|
-
bgWhite: { background: '#ffffff' },
|
|
3658
|
-
bgTeal: { background: '#006666', color: '#ffffff' },
|
|
3659
|
-
textWhite: { color: '#ffffff' },
|
|
3660
|
-
textTeal: { color: '#006666' },
|
|
3661
|
-
textMuted: { color: '#888' },
|
|
3662
|
-
|
|
3663
|
-
// Borders
|
|
3664
|
-
rounded: { borderRadius: '0.375rem' },
|
|
3665
|
-
roundedLg: { borderRadius: '0.5rem' },
|
|
3666
|
-
roundedFull: { borderRadius: '9999px' },
|
|
3667
|
-
border: { border: '1px solid #d8d8d8' },
|
|
3668
|
-
|
|
3669
|
-
// Sizing
|
|
3670
|
-
wFull: { width: '100%' },
|
|
3671
|
-
hFull: { height: '100%' },
|
|
3672
|
-
|
|
3673
|
-
// Transitions
|
|
3674
|
-
transition: { transition: 'all 0.2s ease' }
|
|
3675
|
-
};
|
|
3676
|
-
|
|
3677
3565
|
/**
|
|
3678
3566
|
* Generate responsive CSS with media query breakpoints.
|
|
3679
3567
|
*
|
|
@@ -3795,103 +3683,49 @@ if (bw._isBrowser) {
|
|
|
3795
3683
|
};
|
|
3796
3684
|
}
|
|
3797
3685
|
|
|
3798
|
-
/**
|
|
3799
|
-
* Load the built-in Bootstrap-inspired default stylesheet.
|
|
3800
|
-
*
|
|
3801
|
-
* Injects bitwrench's batteries-included CSS (buttons, cards, grids, forms,
|
|
3802
|
-
* alerts, badges, nav, tabs, etc.) into the document head. Call once at app startup.
|
|
3803
|
-
* Returns null in Node.js (no DOM).
|
|
3804
|
-
*
|
|
3805
|
-
* @param {Object} [options] - Style loading options
|
|
3806
|
-
* @param {boolean} [options.minify=true] - Minify the CSS output
|
|
3807
|
-
* @returns {Element|null} Style element if in browser, null in Node.js
|
|
3808
|
-
* @category CSS & Styling
|
|
3809
|
-
* @see bw.setTheme
|
|
3810
|
-
* @see bw.applyTheme
|
|
3811
|
-
* @see bw.toggleTheme
|
|
3812
|
-
* @example
|
|
3813
|
-
* bw.loadDefaultStyles(); // inject all default CSS
|
|
3814
|
-
*/
|
|
3815
|
-
bw.loadDefaultStyles = function(options = {}) {
|
|
3816
|
-
const { minify = true, palette } = options;
|
|
3817
3686
|
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false, minify: minify });
|
|
3822
|
-
}
|
|
3823
|
-
|
|
3824
|
-
// 2. Inject cosmetic CSS via generateTheme (colors, shadows, radii)
|
|
3825
|
-
var paletteConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, palette || {});
|
|
3826
|
-
var result = bw.generateTheme('', Object.assign({}, paletteConfig, { inject: true }));
|
|
3827
|
-
return result;
|
|
3828
|
-
};
|
|
3687
|
+
// =========================================================================
|
|
3688
|
+
// v2.0.18 Clean Styles API — makeStyles / applyStyles / loadStyles / etc.
|
|
3689
|
+
// =========================================================================
|
|
3829
3690
|
|
|
3691
|
+
/**
|
|
3692
|
+
* Convert a scope selector to a <style> element id.
|
|
3693
|
+
* @private
|
|
3694
|
+
* @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview')
|
|
3695
|
+
* @returns {string} Style element id (e.g. 'bw_style_my_dashboard')
|
|
3696
|
+
*/
|
|
3697
|
+
function _scopeToStyleId(scope) {
|
|
3698
|
+
if (!scope || scope === '' || scope === 'global') return 'bw_style_global';
|
|
3699
|
+
if (scope === 'reset') return 'bw_style_reset';
|
|
3700
|
+
// Strip leading # or . and convert - to _
|
|
3701
|
+
var clean = scope.replace(/^[#.]/, '').replace(/-/g, '_');
|
|
3702
|
+
return 'bw_style_' + clean;
|
|
3703
|
+
}
|
|
3830
3704
|
|
|
3831
3705
|
/**
|
|
3832
|
-
* Generate a complete
|
|
3706
|
+
* Generate a complete styles object from seed colors and layout config.
|
|
3707
|
+
* Pure function — no DOM, no state, no side effects.
|
|
3833
3708
|
*
|
|
3834
|
-
*
|
|
3835
|
-
* forms, nav, tables, tabs, list groups, pagination, progress, hero, utilities)
|
|
3836
|
-
* scoped under `.name` class. Multiple themes can coexist in the stylesheet.
|
|
3837
|
-
* Swap themes by changing the class on a container element.
|
|
3709
|
+
* All parameters are optional. Defaults to the bitwrench default palette.
|
|
3838
3710
|
*
|
|
3839
|
-
* @param {
|
|
3840
|
-
* @param {
|
|
3841
|
-
* @param {string} config.
|
|
3842
|
-
* @param {string} config.
|
|
3843
|
-
* @param {string} [config.tertiary] - Tertiary/accent color hex (defaults to primary)
|
|
3844
|
-
* @param {string} [config.success='#198754'] - Success color hex
|
|
3845
|
-
* @param {string} [config.danger='#dc3545'] - Danger color hex
|
|
3846
|
-
* @param {string} [config.warning='#ffc107'] - Warning color hex
|
|
3847
|
-
* @param {string} [config.info='#0dcaf0'] - Info color hex
|
|
3848
|
-
* @param {string} [config.light='#f8f9fa'] - Light color hex
|
|
3849
|
-
* @param {string} [config.dark='#212529'] - Dark color hex
|
|
3850
|
-
* @param {string} [config.background] - Page background hex (default: '#ffffff' light, derived dark)
|
|
3851
|
-
* @param {string} [config.surface] - Surface/card background hex (default: '#f8f9fa' light, derived dark)
|
|
3711
|
+
* @param {Object} [config] - Style configuration
|
|
3712
|
+
* @param {string} [config.primary='#006666'] - Primary brand color hex
|
|
3713
|
+
* @param {string} [config.secondary='#6c757d'] - Secondary color hex
|
|
3714
|
+
* @param {string} [config.tertiary] - Tertiary color hex (defaults to primary)
|
|
3852
3715
|
* @param {string} [config.spacing='normal'] - 'compact' | 'normal' | 'spacious'
|
|
3853
3716
|
* @param {string} [config.radius='md'] - 'none' | 'sm' | 'md' | 'lg' | 'pill'
|
|
3854
|
-
* @
|
|
3855
|
-
* @param {string|number} [config.typeRatio='normal'] - 'tight' | 'normal' | 'relaxed' | 'dramatic' or a number
|
|
3856
|
-
* @param {string} [config.elevation='md'] - 'flat' | 'sm' | 'md' | 'lg'
|
|
3857
|
-
* @param {string} [config.motion='standard'] - 'reduced' | 'standard' | 'expressive'
|
|
3858
|
-
* @param {number} [config.harmonize=0.20] - 0-1, semantic color hue shift toward primary
|
|
3859
|
-
* @param {boolean} [config.inject=true] - Inject into DOM (browser only)
|
|
3860
|
-
* @returns {Object} { css, palette, name, isLightPrimary, alternate: { css, palette } }
|
|
3717
|
+
* @returns {Object} { css, alternateCss, rules, alternateRules, palette, alternatePalette, isLightPrimary }
|
|
3861
3718
|
* @category CSS & Styling
|
|
3862
|
-
* @see bw.
|
|
3863
|
-
* @see bw.
|
|
3864
|
-
* @see bw.loadDefaultStyles
|
|
3719
|
+
* @see bw.applyStyles
|
|
3720
|
+
* @see bw.loadStyles
|
|
3865
3721
|
* @example
|
|
3866
|
-
*
|
|
3867
|
-
*
|
|
3868
|
-
*
|
|
3869
|
-
* secondary: '#90e0ef',
|
|
3870
|
-
* tertiary: '#00b4d8'
|
|
3871
|
-
* });
|
|
3872
|
-
*
|
|
3873
|
-
* // Apply to a container
|
|
3874
|
-
* document.getElementById('app').classList.add('ocean');
|
|
3875
|
-
*
|
|
3876
|
-
* // Toggle to alternate palette
|
|
3877
|
-
* bw.toggleTheme();
|
|
3878
|
-
*
|
|
3879
|
-
* // Generate CSS for static export (Node.js)
|
|
3880
|
-
* var result = bw.generateTheme('sunset', {
|
|
3881
|
-
* primary: '#e76f51',
|
|
3882
|
-
* secondary: '#264653',
|
|
3883
|
-
* inject: false
|
|
3884
|
-
* });
|
|
3885
|
-
* fs.writeFileSync('sunset.css', result.css + result.alternate.css);
|
|
3722
|
+
* var styles = bw.makeStyles({ primary: '#4f46e5', secondary: '#d97706' });
|
|
3723
|
+
* console.log(styles.palette.primary.base); // '#4f46e5'
|
|
3724
|
+
* // styles.css contains all themed CSS — nothing injected
|
|
3886
3725
|
*/
|
|
3887
|
-
bw.
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
}
|
|
3891
|
-
|
|
3892
|
-
// Merge with defaults; if user didn't supply tertiary, default to their primary
|
|
3893
|
-
var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config);
|
|
3894
|
-
if (!config.tertiary) fullConfig.tertiary = fullConfig.primary;
|
|
3726
|
+
bw.makeStyles = function(config) {
|
|
3727
|
+
var fullConfig = Object.assign({}, DEFAULT_PALETTE_CONFIG, config || {});
|
|
3728
|
+
if (config && !config.tertiary) fullConfig.tertiary = fullConfig.primary;
|
|
3895
3729
|
|
|
3896
3730
|
// Derive primary palette
|
|
3897
3731
|
var palette = derivePalette(fullConfig);
|
|
@@ -3899,131 +3733,207 @@ bw.generateTheme = function(name, config) {
|
|
|
3899
3733
|
// Resolve layout
|
|
3900
3734
|
var layout = resolveLayout(fullConfig);
|
|
3901
3735
|
|
|
3902
|
-
// Generate primary themed CSS rules
|
|
3903
|
-
var themedRules = generateThemedCSS(
|
|
3736
|
+
// Generate primary themed CSS rules (unscoped)
|
|
3737
|
+
var themedRules = generateThemedCSS('', palette, layout);
|
|
3904
3738
|
var cssStr = bw.css(themedRules);
|
|
3905
3739
|
|
|
3906
3740
|
// Derive alternate palette (luminance-inverted)
|
|
3907
3741
|
var altConfig = deriveAlternateConfig(fullConfig);
|
|
3908
3742
|
var altPalette = derivePalette(altConfig);
|
|
3909
3743
|
|
|
3910
|
-
// Generate alternate CSS
|
|
3911
|
-
|
|
3912
|
-
var
|
|
3744
|
+
// Generate alternate CSS rules WITHOUT .bw_theme_alt prefix (raw rules)
|
|
3745
|
+
// applyStyles() wraps them appropriately based on scope
|
|
3746
|
+
var altRawRules = generateThemedCSS('', altPalette, layout);
|
|
3747
|
+
|
|
3748
|
+
// Add body-level surface overrides for the alternate palette.
|
|
3749
|
+
// When .bw_theme_alt is on <html>, ".bw_theme_alt body" correctly matches.
|
|
3750
|
+
altRawRules['body'] = {
|
|
3751
|
+
'color': altPalette.dark.base,
|
|
3752
|
+
'background-color': altPalette.surface || altPalette.light.base
|
|
3753
|
+
};
|
|
3754
|
+
|
|
3755
|
+
var altCssStr = bw.css(altRawRules);
|
|
3913
3756
|
|
|
3914
3757
|
// Determine if primary is light-flavored
|
|
3915
3758
|
var lightPrimary = isLightPalette(fullConfig);
|
|
3916
3759
|
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3760
|
+
return {
|
|
3761
|
+
css: cssStr,
|
|
3762
|
+
alternateCss: altCssStr,
|
|
3763
|
+
rules: themedRules,
|
|
3764
|
+
alternateRules: altRawRules,
|
|
3765
|
+
palette: palette,
|
|
3766
|
+
alternatePalette: altPalette,
|
|
3767
|
+
isLightPrimary: lightPrimary
|
|
3768
|
+
};
|
|
3769
|
+
};
|
|
3926
3770
|
|
|
3927
|
-
|
|
3771
|
+
/**
|
|
3772
|
+
* Inject styles into the DOM with optional scoping.
|
|
3773
|
+
*
|
|
3774
|
+
* Takes a styles object from `makeStyles()` and creates a single `<style>`
|
|
3775
|
+
* element in `<head>`. If a scope selector is provided, all CSS rules are
|
|
3776
|
+
* wrapped under that selector. Alternate CSS is wrapped under `.bw_theme_alt`.
|
|
3777
|
+
*
|
|
3778
|
+
* @param {Object} styles - Result of `bw.makeStyles()`
|
|
3779
|
+
* @param {string} [scope] - Scope selector (e.g. '#my-dashboard', '.preview'). Omit for global.
|
|
3780
|
+
* @returns {Element|null} The `<style>` element, or null in Node.js
|
|
3781
|
+
* @category CSS & Styling
|
|
3782
|
+
* @see bw.makeStyles
|
|
3783
|
+
* @see bw.loadStyles
|
|
3784
|
+
* @see bw.clearStyles
|
|
3785
|
+
* @example
|
|
3786
|
+
* var styles = bw.makeStyles({ primary: '#4f46e5' });
|
|
3787
|
+
* bw.applyStyles(styles); // global
|
|
3788
|
+
* bw.applyStyles(styles, '#my-dashboard'); // scoped
|
|
3789
|
+
*/
|
|
3790
|
+
bw.applyStyles = function(styles, scope) {
|
|
3791
|
+
if (!bw._isBrowser) return null;
|
|
3792
|
+
if (!styles || !styles.rules) {
|
|
3793
|
+
_cw('bw.applyStyles: invalid styles object');
|
|
3794
|
+
return null;
|
|
3928
3795
|
}
|
|
3929
3796
|
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3797
|
+
var styleId = _scopeToStyleId(scope);
|
|
3798
|
+
|
|
3799
|
+
// Scope the primary rules if a scope is provided
|
|
3800
|
+
var primaryRules = styles.rules;
|
|
3801
|
+
if (scope) {
|
|
3802
|
+
primaryRules = scopeRulesUnder(primaryRules, scope);
|
|
3936
3803
|
}
|
|
3937
3804
|
|
|
3938
|
-
//
|
|
3939
|
-
var
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3805
|
+
// Wrap alternate rules with .bw_theme_alt
|
|
3806
|
+
var altRules = styles.alternateRules;
|
|
3807
|
+
if (altRules) {
|
|
3808
|
+
if (scope) {
|
|
3809
|
+
// Scoped compound: #scope.bw_theme_alt .bw_card
|
|
3810
|
+
altRules = scopeRulesUnder(altRules, scope + '.bw_theme_alt');
|
|
3811
|
+
} else {
|
|
3812
|
+
// Global: .bw_theme_alt .bw_card
|
|
3813
|
+
altRules = scopeRulesUnder(altRules, '.bw_theme_alt');
|
|
3947
3814
|
}
|
|
3948
|
-
}
|
|
3949
|
-
bw._activeTheme = result;
|
|
3950
|
-
bw._activeThemeMode = 'primary';
|
|
3815
|
+
}
|
|
3951
3816
|
|
|
3952
|
-
|
|
3817
|
+
// Combine primary + alternate into one CSS string
|
|
3818
|
+
var combined = bw.css(primaryRules);
|
|
3819
|
+
if (altRules) {
|
|
3820
|
+
combined += '\n' + bw.css(altRules);
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
return bw.injectCSS(combined, { id: styleId, append: false });
|
|
3953
3824
|
};
|
|
3954
3825
|
|
|
3955
3826
|
/**
|
|
3956
|
-
*
|
|
3957
|
-
* by adding/removing the `bw_theme_alt` class on `<html>`.
|
|
3827
|
+
* Generate and apply styles in one call. Convenience wrapper.
|
|
3958
3828
|
*
|
|
3959
|
-
*
|
|
3960
|
-
*
|
|
3829
|
+
* Equivalent to: `bw.applyStyles(bw.makeStyles(config), scope)`
|
|
3830
|
+
*
|
|
3831
|
+
* @param {Object} [config] - Style configuration (same as `makeStyles`)
|
|
3832
|
+
* @param {string} [scope] - Scope selector (same as `applyStyles`)
|
|
3833
|
+
* @returns {Element|null} The `<style>` element, or null in Node.js
|
|
3961
3834
|
* @category CSS & Styling
|
|
3962
|
-
* @see bw.
|
|
3963
|
-
* @see bw.
|
|
3835
|
+
* @see bw.makeStyles
|
|
3836
|
+
* @see bw.applyStyles
|
|
3964
3837
|
* @example
|
|
3965
|
-
* bw.
|
|
3966
|
-
* bw.
|
|
3967
|
-
* bw.
|
|
3838
|
+
* bw.loadStyles(); // defaults, global
|
|
3839
|
+
* bw.loadStyles({ primary: '#4f46e5' }); // custom, global
|
|
3840
|
+
* bw.loadStyles({ primary: '#4f46e5' }, '#my-dashboard'); // custom, scoped
|
|
3968
3841
|
*/
|
|
3969
|
-
bw.
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
else if (mode === 'light') wantAlt = !isLight;
|
|
3978
|
-
else if (mode === 'dark') wantAlt = isLight;
|
|
3979
|
-
else wantAlt = false;
|
|
3980
|
-
|
|
3981
|
-
if (wantAlt) {
|
|
3982
|
-
root.classList.add('bw_theme_alt');
|
|
3983
|
-
} else {
|
|
3984
|
-
root.classList.remove('bw_theme_alt');
|
|
3842
|
+
bw.loadStyles = function(config, scope) {
|
|
3843
|
+
// Also inject structural CSS first (only once)
|
|
3844
|
+
if (bw._isBrowser) {
|
|
3845
|
+
var existing = document.getElementById('bw_structural');
|
|
3846
|
+
if (!existing) {
|
|
3847
|
+
var structuralCSS = bw.css(getStructuralStyles());
|
|
3848
|
+
bw.injectCSS(structuralCSS, { id: 'bw_structural', append: false });
|
|
3849
|
+
}
|
|
3985
3850
|
}
|
|
3851
|
+
return bw.applyStyles(bw.makeStyles(config), scope);
|
|
3852
|
+
};
|
|
3986
3853
|
|
|
3987
|
-
|
|
3988
|
-
|
|
3854
|
+
/**
|
|
3855
|
+
* Inject the CSS reset (box-sizing, html/body font, reduced-motion).
|
|
3856
|
+
* Idempotent — if already injected, returns the existing `<style>` element.
|
|
3857
|
+
*
|
|
3858
|
+
* @returns {Element|null} The `<style>` element, or null in Node.js
|
|
3859
|
+
* @category CSS & Styling
|
|
3860
|
+
* @see bw.loadStyles
|
|
3861
|
+
* @see bw.clearStyles
|
|
3862
|
+
* @example
|
|
3863
|
+
* bw.loadReset(); // inject once, safe to call multiple times
|
|
3864
|
+
*/
|
|
3865
|
+
bw.loadReset = function() {
|
|
3866
|
+
if (!bw._isBrowser) return null;
|
|
3867
|
+
var existing = document.getElementById('bw_style_reset');
|
|
3868
|
+
if (existing) return existing;
|
|
3869
|
+
return bw.injectCSS(bw.css(getResetStyles()), { id: 'bw_style_reset', append: false });
|
|
3989
3870
|
};
|
|
3990
3871
|
|
|
3991
3872
|
/**
|
|
3992
|
-
* Toggle between primary and alternate
|
|
3873
|
+
* Toggle between primary and alternate palettes.
|
|
3993
3874
|
*
|
|
3875
|
+
* Adds/removes the `bw_theme_alt` class on the scoping element.
|
|
3876
|
+
* Without a scope, toggles on `<html>` (global).
|
|
3877
|
+
* With a scope, toggles on the first matching element.
|
|
3878
|
+
*
|
|
3879
|
+
* @param {string} [scope] - Scope selector (e.g. '#my-dashboard'). Omit for global.
|
|
3994
3880
|
* @returns {string} Active mode after toggle: 'primary' or 'alternate'
|
|
3995
3881
|
* @category CSS & Styling
|
|
3996
|
-
* @see bw.
|
|
3997
|
-
* @see bw.
|
|
3882
|
+
* @see bw.applyStyles
|
|
3883
|
+
* @see bw.clearStyles
|
|
3998
3884
|
* @example
|
|
3999
|
-
* bw.
|
|
3885
|
+
* bw.toggleStyles(); // global toggle on <html>
|
|
3886
|
+
* bw.toggleStyles('#my-dashboard'); // scoped toggle
|
|
4000
3887
|
*/
|
|
4001
|
-
bw.
|
|
4002
|
-
|
|
4003
|
-
|
|
3888
|
+
bw.toggleStyles = function(scope) {
|
|
3889
|
+
if (!bw._isBrowser) return 'primary';
|
|
3890
|
+
var target;
|
|
3891
|
+
if (scope) {
|
|
3892
|
+
var els = bw.$(scope);
|
|
3893
|
+
target = els[0];
|
|
3894
|
+
} else {
|
|
3895
|
+
target = document.documentElement;
|
|
3896
|
+
}
|
|
3897
|
+
if (!target) return 'primary';
|
|
3898
|
+
|
|
3899
|
+
var hasAlt = target.classList.contains('bw_theme_alt');
|
|
3900
|
+
if (hasAlt) {
|
|
3901
|
+
target.classList.remove('bw_theme_alt');
|
|
3902
|
+
return 'primary';
|
|
3903
|
+
} else {
|
|
3904
|
+
target.classList.add('bw_theme_alt');
|
|
3905
|
+
return 'alternate';
|
|
3906
|
+
}
|
|
4004
3907
|
};
|
|
4005
3908
|
|
|
4006
3909
|
/**
|
|
4007
|
-
* Remove
|
|
4008
|
-
* Use this before generating a new theme with a different name to prevent
|
|
4009
|
-
* stale CSS accumulation.
|
|
3910
|
+
* Remove injected styles for a given scope.
|
|
4010
3911
|
*
|
|
3912
|
+
* Finds the `<style>` element by id and removes it. Also removes
|
|
3913
|
+
* the `bw_theme_alt` class from the relevant element.
|
|
3914
|
+
*
|
|
3915
|
+
* @param {string} [scope] - Scope selector. Omit to remove global styles.
|
|
4011
3916
|
* @category CSS & Styling
|
|
4012
|
-
* @see bw.
|
|
3917
|
+
* @see bw.applyStyles
|
|
3918
|
+
* @see bw.loadStyles
|
|
4013
3919
|
* @example
|
|
4014
|
-
* bw.
|
|
4015
|
-
* bw.
|
|
3920
|
+
* bw.clearStyles(); // remove global styles
|
|
3921
|
+
* bw.clearStyles('#my-dashboard'); // remove scoped styles
|
|
3922
|
+
* bw.clearStyles('reset'); // remove the CSS reset
|
|
4016
3923
|
*/
|
|
4017
|
-
bw.
|
|
4018
|
-
if (bw.
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
3924
|
+
bw.clearStyles = function(scope) {
|
|
3925
|
+
if (!bw._isBrowser) return;
|
|
3926
|
+
var styleId = _scopeToStyleId(scope);
|
|
3927
|
+
var el = document.getElementById(styleId);
|
|
3928
|
+
if (el) el.remove();
|
|
3929
|
+
|
|
3930
|
+
// Also remove bw_theme_alt from the relevant element
|
|
3931
|
+
if (scope && scope !== 'reset' && scope !== 'global') {
|
|
3932
|
+
var targets = bw.$(scope);
|
|
3933
|
+
if (targets[0]) targets[0].classList.remove('bw_theme_alt');
|
|
3934
|
+
} else if (!scope || scope === 'global') {
|
|
3935
|
+
document.documentElement.classList.remove('bw_theme_alt');
|
|
4024
3936
|
}
|
|
4025
|
-
bw._activeTheme = null;
|
|
4026
|
-
bw._activeThemeMode = 'primary';
|
|
4027
3937
|
};
|
|
4028
3938
|
|
|
4029
3939
|
// Expose color utility functions on bw namespace
|
|
@@ -4246,10 +4156,15 @@ bw.copyToClipboard = function(text) {
|
|
|
4246
4156
|
* @param {Object} config - Table configuration
|
|
4247
4157
|
* @param {Array<Object>} config.data - Array of row objects to display
|
|
4248
4158
|
* @param {Array<Object>} [config.columns] - Column definitions with key, label, render
|
|
4249
|
-
* @param {string} [config.className='
|
|
4159
|
+
* @param {string} [config.className=''] - Additional CSS classes for table element
|
|
4250
4160
|
* @param {boolean} [config.sortable=true] - Enable click-to-sort headers
|
|
4251
4161
|
* @param {Function} [config.onSort] - Sort callback (column, direction)
|
|
4252
|
-
* @
|
|
4162
|
+
* @param {boolean} [config.selectable=false] - Enable row selection on click
|
|
4163
|
+
* @param {Function} [config.onRowClick] - Row click callback (row, index, event)
|
|
4164
|
+
* @param {number} [config.pageSize] - Rows per page (enables pagination when set)
|
|
4165
|
+
* @param {number} [config.currentPage=1] - Current page number (1-based)
|
|
4166
|
+
* @param {Function} [config.onPageChange] - Page change callback (newPage)
|
|
4167
|
+
* @returns {Object} TACO object for table (with optional pagination controls)
|
|
4253
4168
|
* @category Component Builders
|
|
4254
4169
|
* @see bw.makeDataTable
|
|
4255
4170
|
* @example
|
|
@@ -4261,7 +4176,12 @@ bw.copyToClipboard = function(text) {
|
|
|
4261
4176
|
* columns: [
|
|
4262
4177
|
* { key: 'name', label: 'Name' },
|
|
4263
4178
|
* { key: 'age', label: 'Age' }
|
|
4264
|
-
* ]
|
|
4179
|
+
* ],
|
|
4180
|
+
* selectable: true,
|
|
4181
|
+
* onRowClick: function(row, i) { console.log('clicked', row.name); },
|
|
4182
|
+
* pageSize: 10,
|
|
4183
|
+
* currentPage: 1,
|
|
4184
|
+
* onPageChange: function(page) { console.log('page', page); }
|
|
4265
4185
|
* });
|
|
4266
4186
|
*/
|
|
4267
4187
|
bw.makeTable = function(config) {
|
|
@@ -4274,41 +4194,47 @@ bw.makeTable = function(config) {
|
|
|
4274
4194
|
sortable = true,
|
|
4275
4195
|
onSort,
|
|
4276
4196
|
sortColumn,
|
|
4277
|
-
sortDirection = 'asc'
|
|
4197
|
+
sortDirection = 'asc',
|
|
4198
|
+
selectable = false,
|
|
4199
|
+
onRowClick,
|
|
4200
|
+
pageSize,
|
|
4201
|
+
currentPage = 1,
|
|
4202
|
+
onPageChange
|
|
4278
4203
|
} = config;
|
|
4279
4204
|
|
|
4280
|
-
// Build class list: always include bw_table, add striped/hover, append user className
|
|
4205
|
+
// Build class list: always include bw_table, add striped/hover/selectable, append user className
|
|
4281
4206
|
let cls = 'bw_table';
|
|
4282
4207
|
if (striped) cls += ' bw_table_striped';
|
|
4283
|
-
if (hover) cls += ' bw_table_hover';
|
|
4208
|
+
if (hover || selectable) cls += ' bw_table_hover';
|
|
4209
|
+
if (selectable) cls += ' bw_table_selectable';
|
|
4284
4210
|
if (className) cls += ' ' + className;
|
|
4285
4211
|
cls = cls.trim();
|
|
4286
|
-
|
|
4212
|
+
|
|
4287
4213
|
// Auto-detect columns if not provided
|
|
4288
|
-
const cols = columns || (data.length > 0
|
|
4214
|
+
const cols = columns || (data.length > 0
|
|
4289
4215
|
? _keys(data[0]).map(key => ({ key, label: key }))
|
|
4290
4216
|
: []);
|
|
4291
|
-
|
|
4217
|
+
|
|
4292
4218
|
// Current sort state
|
|
4293
4219
|
let currentSortColumn = sortColumn || null;
|
|
4294
4220
|
let currentSortDirection = sortDirection;
|
|
4295
|
-
|
|
4221
|
+
|
|
4296
4222
|
// Sort data if column specified
|
|
4297
4223
|
let sortedData = [...data];
|
|
4298
4224
|
if (currentSortColumn) {
|
|
4299
4225
|
sortedData.sort((a, b) => {
|
|
4300
4226
|
const aVal = a[currentSortColumn];
|
|
4301
4227
|
const bVal = b[currentSortColumn];
|
|
4302
|
-
|
|
4228
|
+
|
|
4303
4229
|
// Handle different types
|
|
4304
4230
|
if (_is(aVal, 'number') && _is(bVal, 'number')) {
|
|
4305
4231
|
return currentSortDirection === 'asc' ? aVal - bVal : bVal - aVal;
|
|
4306
4232
|
}
|
|
4307
|
-
|
|
4233
|
+
|
|
4308
4234
|
// String comparison
|
|
4309
4235
|
const aStr = String(aVal || '').toLowerCase();
|
|
4310
4236
|
const bStr = String(bVal || '').toLowerCase();
|
|
4311
|
-
|
|
4237
|
+
|
|
4312
4238
|
if (currentSortDirection === 'asc') {
|
|
4313
4239
|
return aStr.localeCompare(bStr);
|
|
4314
4240
|
} else {
|
|
@@ -4316,23 +4242,32 @@ bw.makeTable = function(config) {
|
|
|
4316
4242
|
}
|
|
4317
4243
|
});
|
|
4318
4244
|
}
|
|
4319
|
-
|
|
4245
|
+
|
|
4246
|
+
// Pagination
|
|
4247
|
+
const totalRows = sortedData.length;
|
|
4248
|
+
const totalPages = pageSize ? Math.max(1, Math.ceil(totalRows / pageSize)) : 1;
|
|
4249
|
+
const page = Math.max(1, Math.min(currentPage, totalPages));
|
|
4250
|
+
if (pageSize) {
|
|
4251
|
+
const start = (page - 1) * pageSize;
|
|
4252
|
+
sortedData = sortedData.slice(start, start + pageSize);
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4320
4255
|
// Create sort handler
|
|
4321
4256
|
const handleSort = (column) => {
|
|
4322
4257
|
if (!sortable) return;
|
|
4323
|
-
|
|
4258
|
+
|
|
4324
4259
|
if (currentSortColumn === column) {
|
|
4325
4260
|
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
|
|
4326
4261
|
} else {
|
|
4327
4262
|
currentSortColumn = column;
|
|
4328
4263
|
currentSortDirection = 'asc';
|
|
4329
4264
|
}
|
|
4330
|
-
|
|
4265
|
+
|
|
4331
4266
|
if (onSort) {
|
|
4332
4267
|
onSort(column, currentSortDirection);
|
|
4333
4268
|
}
|
|
4334
4269
|
};
|
|
4335
|
-
|
|
4270
|
+
|
|
4336
4271
|
// Build table header
|
|
4337
4272
|
const thead = {
|
|
4338
4273
|
t: 'thead',
|
|
@@ -4355,24 +4290,87 @@ bw.makeTable = function(config) {
|
|
|
4355
4290
|
}))
|
|
4356
4291
|
}
|
|
4357
4292
|
};
|
|
4358
|
-
|
|
4359
|
-
// Build table body
|
|
4293
|
+
|
|
4294
|
+
// Build table body with selectable/onRowClick support
|
|
4360
4295
|
const tbody = {
|
|
4361
4296
|
t: 'tbody',
|
|
4362
|
-
c: sortedData.map(row =>
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4297
|
+
c: sortedData.map((row, idx) => {
|
|
4298
|
+
const globalIdx = pageSize ? (page - 1) * pageSize + idx : idx;
|
|
4299
|
+
const rowAttrs = {};
|
|
4300
|
+
if (selectable || onRowClick) {
|
|
4301
|
+
rowAttrs.style = 'cursor:pointer;';
|
|
4302
|
+
rowAttrs.onclick = function(e) {
|
|
4303
|
+
if (selectable) {
|
|
4304
|
+
// Toggle selected class on this row
|
|
4305
|
+
var tr = e.currentTarget;
|
|
4306
|
+
tr.classList.toggle('bw_table_row_selected');
|
|
4307
|
+
}
|
|
4308
|
+
if (onRowClick) {
|
|
4309
|
+
onRowClick(row, globalIdx, e);
|
|
4310
|
+
}
|
|
4311
|
+
};
|
|
4312
|
+
}
|
|
4313
|
+
return {
|
|
4314
|
+
t: 'tr',
|
|
4315
|
+
a: rowAttrs,
|
|
4316
|
+
c: cols.map(col => ({
|
|
4317
|
+
t: 'td',
|
|
4318
|
+
c: col.render ? col.render(row[col.key], row) : String(row[col.key] || '')
|
|
4319
|
+
}))
|
|
4320
|
+
};
|
|
4321
|
+
})
|
|
4369
4322
|
};
|
|
4370
|
-
|
|
4371
|
-
|
|
4323
|
+
|
|
4324
|
+
const table = {
|
|
4372
4325
|
t: 'table',
|
|
4373
4326
|
a: { class: cls },
|
|
4374
4327
|
c: [thead, tbody]
|
|
4375
4328
|
};
|
|
4329
|
+
|
|
4330
|
+
// If no pagination, return table directly
|
|
4331
|
+
if (!pageSize) return table;
|
|
4332
|
+
|
|
4333
|
+
// Build pagination controls
|
|
4334
|
+
const pageButtons = [];
|
|
4335
|
+
// Previous button
|
|
4336
|
+
pageButtons.push({
|
|
4337
|
+
t: 'button',
|
|
4338
|
+
a: {
|
|
4339
|
+
class: 'bw_btn bw_btn_sm',
|
|
4340
|
+
disabled: page <= 1 ? 'disabled' : undefined,
|
|
4341
|
+
onclick: page > 1 && onPageChange ? function() { onPageChange(page - 1); } : undefined
|
|
4342
|
+
},
|
|
4343
|
+
c: 'Prev'
|
|
4344
|
+
});
|
|
4345
|
+
// Page info
|
|
4346
|
+
pageButtons.push({
|
|
4347
|
+
t: 'span',
|
|
4348
|
+
a: { style: 'margin:0 0.5rem;font-size:0.875rem;' },
|
|
4349
|
+
c: 'Page ' + page + ' of ' + totalPages
|
|
4350
|
+
});
|
|
4351
|
+
// Next button
|
|
4352
|
+
pageButtons.push({
|
|
4353
|
+
t: 'button',
|
|
4354
|
+
a: {
|
|
4355
|
+
class: 'bw_btn bw_btn_sm',
|
|
4356
|
+
disabled: page >= totalPages ? 'disabled' : undefined,
|
|
4357
|
+
onclick: page < totalPages && onPageChange ? function() { onPageChange(page + 1); } : undefined
|
|
4358
|
+
},
|
|
4359
|
+
c: 'Next'
|
|
4360
|
+
});
|
|
4361
|
+
|
|
4362
|
+
return {
|
|
4363
|
+
t: 'div',
|
|
4364
|
+
a: { class: 'bw_table_paginated' },
|
|
4365
|
+
c: [
|
|
4366
|
+
table,
|
|
4367
|
+
{
|
|
4368
|
+
t: 'div',
|
|
4369
|
+
a: { class: 'bw_table_pagination', style: 'display:flex;align-items:center;justify-content:flex-end;padding:0.5rem 0;gap:0.25rem;' },
|
|
4370
|
+
c: pageButtons
|
|
4371
|
+
}
|
|
4372
|
+
]
|
|
4373
|
+
};
|
|
4376
4374
|
};
|
|
4377
4375
|
|
|
4378
4376
|
/**
|