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
@@ -4,6 +4,13 @@
4
4
  * Provides bw.highlight() for tokenizing JS/CSS/HTML into TACO spans,
5
5
  * and bw.codeEditor() for a live editable code block with syntax coloring.
6
6
  *
7
+ * Theme integration: The editor chrome (background, text color, font) reads
8
+ * from CSS custom properties --bw_code_bg, --bw_code_text, --bw_font_mono,
9
+ * falling back to built-in dark values when no theme is active. Syntax
10
+ * highlighting colors are intentionally fixed (they are a code color scheme,
11
+ * not brand colors). The bw_ce_light class is still supported for manual
12
+ * light-mode override.
13
+ *
7
14
  * Can be loaded standalone (browser script tag after bitwrench.umd.js),
8
15
  * or imported as an ES module / CJS module.
9
16
  *
@@ -14,53 +21,58 @@
14
21
  // -- CSS (injected once) ----------------------------------------------
15
22
  var _cssInjected = false;
16
23
  var CSS_TEXT =
17
- '.bw-ce{background:#1e293b;border-radius:6px;border:1px solid rgba(255,255,255,0.08);overflow:auto}' +
18
- '.bw-ce pre{margin:0;padding:0}' +
19
- '.bw-ce code{font-family:"SF Mono",Monaco,"Cascadia Code",Consolas,monospace;font-size:0.875rem;line-height:1.6;color:#e2e8f0;outline:none;white-space:pre-wrap;display:block;padding:0.75rem 1rem}' +
20
- '.bw-ce code:empty::before{content:"\\200b"}' +
21
- '.bw-ce .bw-ce-keyword{color:#c792ea}' +
22
- '.bw-ce .bw-ce-string{color:#c3e88d}' +
23
- '.bw-ce .bw-ce-comment{color:#546e7a;font-style:italic}' +
24
- '.bw-ce .bw-ce-number{color:#f78c6c}' +
25
- '.bw-ce .bw-ce-operator{color:#89ddff}' +
26
- '.bw-ce .bw-ce-punctuation{color:#89ddff}' +
27
- '.bw-ce .bw-ce-property{color:#82aaff}' +
28
- '.bw-ce .bw-ce-function{color:#82aaff}' +
29
- '.bw-ce .bw-ce-tag{color:#f07178}' +
30
- '.bw-ce .bw-ce-attr-name{color:#ffcb6b}' +
31
- '.bw-ce .bw-ce-attr-value{color:#c3e88d}' +
32
- '.bw-ce .bw-ce-selector{color:#c792ea}' +
33
- '.bw-ce .bw-ce-css-prop{color:#82aaff}' +
34
- '.bw-ce .bw-ce-css-value{color:#f78c6c}' +
35
- '.bw-ce .bw-ce-at-rule{color:#c792ea;font-style:italic}' +
36
- '.bw-ce .bw-ce-color{color:#f78c6c}' +
37
- '.bw-ce .bw-ce-template-interp{color:#89ddff}' +
24
+ '.bw_ce{background:var(--bw_code_bg,#1e293b);border-radius:6px;border:1px solid rgba(255,255,255,0.08);overflow:auto}' +
25
+ '.bw_ce pre{margin:0;padding:0}' +
26
+ '.bw_ce code{font-family:var(--bw_font_mono,"SF Mono",Monaco,"Cascadia Code",Consolas,monospace);font-size:0.875rem;line-height:1.6;color:var(--bw_code_text,#e2e8f0);outline:none;white-space:pre-wrap;display:block;padding:0.75rem 1rem}' +
27
+ '.bw_ce code:empty::before{content:"\\200b"}' +
28
+ '.bw_ce .bw_ce_keyword{color:#c792ea}' +
29
+ '.bw_ce .bw_ce_string{color:#c3e88d}' +
30
+ '.bw_ce .bw_ce_comment{color:#546e7a;font-style:italic}' +
31
+ '.bw_ce .bw_ce_number{color:#f78c6c}' +
32
+ '.bw_ce .bw_ce_operator{color:#89ddff}' +
33
+ '.bw_ce .bw_ce_punctuation{color:#89ddff}' +
34
+ '.bw_ce .bw_ce_property{color:#82aaff}' +
35
+ '.bw_ce .bw_ce_function{color:#82aaff}' +
36
+ '.bw_ce .bw_ce_tag{color:#f07178}' +
37
+ '.bw_ce .bw_ce_attr_name{color:#ffcb6b}' +
38
+ '.bw_ce .bw_ce_attr_value{color:#c3e88d}' +
39
+ '.bw_ce .bw_ce_selector{color:#c792ea}' +
40
+ '.bw_ce .bw_ce_css_prop{color:#82aaff}' +
41
+ '.bw_ce .bw_ce_css_value{color:#f78c6c}' +
42
+ '.bw_ce .bw_ce_at_rule{color:#c792ea;font-style:italic}' +
43
+ '.bw_ce .bw_ce_color{color:#f78c6c}' +
44
+ '.bw_ce .bw_ce_template_interp{color:#89ddff}' +
38
45
  // Light theme
39
- '.bw-ce-light.bw-ce{background:#fafafa;border-color:#d8d8d8}' +
40
- '.bw-ce-light.bw-ce code{color:#1a1a1a}' +
41
- '.bw-ce-light.bw-ce .bw-ce-keyword{color:#7c3aed}' +
42
- '.bw-ce-light.bw-ce .bw-ce-string{color:#16a34a}' +
43
- '.bw-ce-light.bw-ce .bw-ce-comment{color:#9ca3af;font-style:italic}' +
44
- '.bw-ce-light.bw-ce .bw-ce-number{color:#ea580c}' +
45
- '.bw-ce-light.bw-ce .bw-ce-operator{color:#0891b2}' +
46
- '.bw-ce-light.bw-ce .bw-ce-punctuation{color:#6b7280}' +
47
- '.bw-ce-light.bw-ce .bw-ce-property{color:#2563eb}' +
48
- '.bw-ce-light.bw-ce .bw-ce-function{color:#2563eb}' +
49
- '.bw-ce-light.bw-ce .bw-ce-tag{color:#dc2626}' +
50
- '.bw-ce-light.bw-ce .bw-ce-attr-name{color:#d97706}' +
51
- '.bw-ce-light.bw-ce .bw-ce-attr-value{color:#16a34a}' +
52
- '.bw-ce-light.bw-ce .bw-ce-selector{color:#7c3aed}' +
53
- '.bw-ce-light.bw-ce .bw-ce-css-prop{color:#2563eb}' +
54
- '.bw-ce-light.bw-ce .bw-ce-css-value{color:#ea580c}' +
55
- '.bw-ce-light.bw-ce .bw-ce-at-rule{color:#7c3aed}' +
56
- '.bw-ce-light.bw-ce .bw-ce-color{color:#ea580c}' +
57
- '.bw-ce-light.bw-ce .bw-ce-template-interp{color:#0891b2}';
46
+ '.bw_ce_light.bw_ce{background:#fafafa;border-color:#d8d8d8}' +
47
+ '.bw_ce_light.bw_ce code{color:#1a1a1a}' +
48
+ '.bw_ce_light.bw_ce .bw_ce_keyword{color:#7c3aed}' +
49
+ '.bw_ce_light.bw_ce .bw_ce_string{color:#16a34a}' +
50
+ '.bw_ce_light.bw_ce .bw_ce_comment{color:#9ca3af;font-style:italic}' +
51
+ '.bw_ce_light.bw_ce .bw_ce_number{color:#ea580c}' +
52
+ '.bw_ce_light.bw_ce .bw_ce_operator{color:#0891b2}' +
53
+ '.bw_ce_light.bw_ce .bw_ce_punctuation{color:#6b7280}' +
54
+ '.bw_ce_light.bw_ce .bw_ce_property{color:#2563eb}' +
55
+ '.bw_ce_light.bw_ce .bw_ce_function{color:#2563eb}' +
56
+ '.bw_ce_light.bw_ce .bw_ce_tag{color:#dc2626}' +
57
+ '.bw_ce_light.bw_ce .bw_ce_attr_name{color:#d97706}' +
58
+ '.bw_ce_light.bw_ce .bw_ce_attr_value{color:#16a34a}' +
59
+ '.bw_ce_light.bw_ce .bw_ce_selector{color:#7c3aed}' +
60
+ '.bw_ce_light.bw_ce .bw_ce_css_prop{color:#2563eb}' +
61
+ '.bw_ce_light.bw_ce .bw_ce_css_value{color:#ea580c}' +
62
+ '.bw_ce_light.bw_ce .bw_ce_at_rule{color:#7c3aed}' +
63
+ '.bw_ce_light.bw_ce .bw_ce_color{color:#ea580c}' +
64
+ '.bw_ce_light.bw_ce .bw_ce_template_interp{color:#0891b2}' +
65
+ // Line number gutter (opt-in via lineNumbers option)
66
+ '.bw_ce_wrap{display:flex;flex-direction:row}' +
67
+ '.bw_ce_gutter{flex:0 0 auto;padding:0.75rem 0;text-align:right;user-select:none;-webkit-user-select:none;color:#546e7a;font-family:var(--bw_font_mono,"SF Mono",Monaco,"Cascadia Code",Consolas,monospace);font-size:0.875rem;line-height:1.6;border-right:1px solid rgba(255,255,255,0.08);overflow:hidden}' +
68
+ '.bw_ce_gutter span{display:block;padding:0 0.5rem 0 0.75rem}' +
69
+ '.bw_ce_light .bw_ce_gutter{color:#9ca3af;border-right-color:#d8d8d8}';
58
70
 
59
71
  function ensureCSS(bw) {
60
72
  if (_cssInjected) return;
61
73
  _cssInjected = true;
62
74
  if (bw && bw.injectCSS) {
63
- bw.injectCSS(CSS_TEXT, { id: 'bw-code-edit-styles' });
75
+ bw.injectCSS(CSS_TEXT, { id: 'bw_code_edit_styles' });
64
76
  }
65
77
  }
66
78
 
@@ -483,7 +495,7 @@ function tokensToTACO(tokArr) {
483
495
  if (tok.type === 'plain') {
484
496
  result.push(tok.text);
485
497
  } else {
486
- result.push({ t: 'span', a: { class: 'bw-ce-' + tok.type }, c: tok.text });
498
+ result.push({ t: 'span', a: { class: 'bw_ce_' + tok.type }, c: tok.text });
487
499
  }
488
500
  }
489
501
  return result;
@@ -537,46 +549,83 @@ function codeEditor(opts) {
537
549
  var lang = opts.lang || 'js';
538
550
  var height = opts.height || '180px';
539
551
  var readOnly = !!opts.readOnly;
540
- var className = 'bw-ce' + (opts.className ? ' ' + opts.className : '');
552
+ var showLineNumbers = !!opts.lineNumbers;
553
+ var className = 'bw_ce' + (opts.className ? ' ' + opts.className : '');
541
554
 
542
555
  var highlighted = highlight(code, lang);
543
556
 
544
557
  var codeAttrs = {
545
558
  spellcheck: 'false',
546
- class: 'bw-ce-code'
559
+ class: 'bw_ce_code'
547
560
  };
548
561
  if (!readOnly) {
549
562
  codeAttrs.contenteditable = 'true';
550
563
  }
551
564
 
565
+ // Build line number gutter TACO if requested
566
+ var gutterTaco = null;
567
+ if (showLineNumbers) {
568
+ var lineCount = (code.match(/\n/g) || []).length + 1;
569
+ var gutterLines = [];
570
+ for (var li = 1; li <= lineCount; li++) {
571
+ gutterLines.push({ t: 'span', c: String(li) });
572
+ }
573
+ gutterTaco = { t: 'div', a: { class: 'bw_ce_gutter' }, c: gutterLines };
574
+ }
575
+
576
+ var preBlock = { t: 'pre', a: { style: 'flex:1;min-width:0;margin:0' }, c: { t: 'code', a: codeAttrs, c: highlighted } };
577
+ var innerContent = showLineNumbers
578
+ ? { t: 'div', a: { class: 'bw_ce_wrap' }, c: [gutterTaco, preBlock] }
579
+ : preBlock;
580
+
552
581
  return {
553
582
  t: 'div',
554
583
  a: { class: className, style: 'max-height:' + height + ';overflow:auto' },
555
- c: [
556
- { t: 'pre', c: { t: 'code', a: codeAttrs, c: highlighted } }
557
- ],
584
+ c: [innerContent],
558
585
  o: {
559
586
  mounted: function(el) {
560
- var codeEl = el.querySelector('.bw-ce-code');
587
+ var codeEl = el.querySelector('.bw_ce_code');
561
588
  if (!codeEl) return;
562
589
 
563
590
  var currentCode = code;
564
591
  var debounceTimer = null;
592
+ var gutterEl = showLineNumbers ? el.querySelector('.bw_ce_gutter') : null;
565
593
 
566
594
  // Resolve bw from global or import context
567
595
  var bw = (typeof window !== 'undefined' && window.bw) || {};
568
596
 
569
597
  function getValue() { return codeEl.textContent || ''; }
570
598
 
599
+ function updateGutter(text) {
600
+ if (!gutterEl) return;
601
+ var count = (text.match(/\n/g) || []).length + 1;
602
+ var html = '';
603
+ for (var i = 1; i <= count; i++) html += '<span>' + i + '</span>';
604
+ gutterEl.innerHTML = html;
605
+ }
606
+
571
607
  function setValue(newCode) {
572
608
  currentCode = newCode;
573
609
  var tacos = highlight(newCode, lang);
574
610
  if (bw.html) codeEl.innerHTML = bw.html({ t: 'span', c: tacos });
611
+ updateGutter(newCode);
575
612
  }
576
613
 
577
614
  // Expose API on the element
578
615
  el._bwCodeEdit = { getValue: getValue, setValue: setValue };
579
616
 
617
+ // Scroll sync: keep gutter aligned with code
618
+ if (gutterEl) {
619
+ var scrollParent = codeEl.closest('.bw_ce') || el;
620
+ scrollParent.addEventListener('scroll', function() {
621
+ gutterEl.style.transform = 'translateY(' + (-scrollParent.scrollTop) + 'px)';
622
+ });
623
+ // If the outer .bw_ce has overflow, sync from there
624
+ el.addEventListener('scroll', function() {
625
+ gutterEl.style.transform = 'translateY(' + (-el.scrollTop) + 'px)';
626
+ });
627
+ }
628
+
580
629
  if (readOnly) return;
581
630
 
582
631
  function rehighlight() {
@@ -587,6 +636,7 @@ function codeEditor(opts) {
587
636
  var tacos = highlight(newCode, lang);
588
637
  if (bw.html) codeEl.innerHTML = bw.html({ t: 'span', c: tacos });
589
638
  setCaretOffset(codeEl, offset);
639
+ updateGutter(newCode);
590
640
  if (opts.onChange) opts.onChange(newCode);
591
641
  }
592
642
 
@@ -6,9 +6,13 @@
6
6
  * bw.colorParse, bw.colorRgbToHsl, etc.
7
7
  *
8
8
  * @module bitwrench-color-utils
9
- * @license BSD-2-Clause
9
+ * @license BSD-2-Clause
10
+ * @copy Manu Chatterjee @deftio
10
11
  */
11
12
 
13
+ function _xs (x) {
14
+ return ('0' + x.toString(16)).slice(-2)
15
+ }
12
16
  /**
13
17
  * Clamp a value between min and max.
14
18
  * @param {number} val
@@ -200,10 +204,7 @@ export function hexToHsl(hex) {
200
204
  */
201
205
  export function hslToHex(hsl) {
202
206
  var rgb = colorHslToRgb(hsl[0], hsl[1], hsl[2], 255, true);
203
- return '#' +
204
- ('0' + rgb[0].toString(16)).slice(-2) +
205
- ('0' + rgb[1].toString(16)).slice(-2) +
206
- ('0' + rgb[2].toString(16)).slice(-2);
207
+ return '#' + _xs(rgb[0])+_xs(rgb[1])+_xs(rgb[2]);
207
208
  }
208
209
 
209
210
  /**
@@ -232,10 +233,7 @@ export function mixColor(hex1, hex2, ratio) {
232
233
  var r = Math.round(c1[0] + (c2[0] - c1[0]) * ratio);
233
234
  var g = Math.round(c1[1] + (c2[1] - c1[1]) * ratio);
234
235
  var b = Math.round(c1[2] + (c2[2] - c1[2]) * ratio);
235
- return '#' +
236
- ('0' + r.toString(16)).slice(-2) +
237
- ('0' + g.toString(16)).slice(-2) +
238
- ('0' + b.toString(16)).slice(-2);
236
+ return '#' + _xs(r) + _xs(g) + _xs(b);
239
237
  }
240
238
 
241
239
  /**
@@ -415,16 +413,24 @@ export function derivePalette(config) {
415
413
  var lightBase = config.light || hslToHex([h, 8, 97]);
416
414
  var darkBase = config.dark || hslToHex([h, 10, 13]);
417
415
 
416
+ // Background & surface tokens — tinted with primary hue for theme personality.
417
+ // Very subtle: bg at L=98/S=6, surface at L=96/S=8.
418
+ // User can override with config.background / config.surface.
419
+ var bgBase = config.background || hslToHex([h, 6, 98]);
420
+ var surfBase = config.surface || hslToHex([h, 8, 96]);
421
+
418
422
  var palette = {
419
- primary: deriveShades(config.primary),
420
- secondary: deriveShades(config.secondary),
421
- tertiary: deriveShades(config.tertiary),
422
- success: deriveShades(successBase),
423
- danger: deriveShades(dangerBase),
424
- warning: deriveShades(warningBase),
425
- info: deriveShades(infoBase),
426
- light: deriveShades(lightBase),
427
- dark: deriveShades(darkBase)
423
+ primary: deriveShades(config.primary),
424
+ secondary: deriveShades(config.secondary),
425
+ tertiary: deriveShades(config.tertiary),
426
+ success: deriveShades(successBase),
427
+ danger: deriveShades(dangerBase),
428
+ warning: deriveShades(warningBase),
429
+ info: deriveShades(infoBase),
430
+ light: deriveShades(lightBase),
431
+ dark: deriveShades(darkBase),
432
+ background: bgBase,
433
+ surface: surfBase
428
434
  };
429
435
 
430
436
  return palette;
@@ -1,5 +1,8 @@
1
1
  /**
2
- * Empty stub for bitwrench-components-v2.js.
2
+ * Empty stub for bitwrench-bccl.js.
3
3
  * Used by the lean build to exclude all BCCL component code.
4
4
  */
5
5
  export const componentHandles = {};
6
+ export function variantClass() { return ''; }
7
+ export var BCCL = {};
8
+ export function make() { throw new Error('bw.make() requires the full bitwrench build (not lean)'); }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Bitwrench v2 File I/O Functions
3
+ *
4
+ * Save/load files in both Node.js and browser environments.
5
+ * Node uses fs module, browser uses Blob/XHR/FileReader.
6
+ *
7
+ * Called via bindFileOps(bw) which attaches all functions to the bw namespace.
8
+ * This preserves the same public API (bw.saveClientFile, bw.loadClientFile, etc.)
9
+ * while keeping the implementation in a separate module.
10
+ *
11
+ * @module bitwrench-file-ops
12
+ * @license BSD-2-Clause
13
+ * @author M A Chatterjee <deftio [at] deftio [dot] com>
14
+ */
15
+
16
+ /**
17
+ * Attach all file I/O functions to the bitwrench namespace.
18
+ *
19
+ * @param {Object} bw - Bitwrench namespace object
20
+ */
21
+ export function bindFileOps(bw) {
22
+
23
+ /**
24
+ * Save data to a file. Works in both Node.js (fs.writeFile) and browser (download link).
25
+ *
26
+ * @param {string} fname - Filename to save as
27
+ * @param {*} data - Data to save (string or buffer)
28
+ * @category File I/O
29
+ */
30
+ bw.saveClientFile = function(fname, data) {
31
+ if (bw.isNodeJS()) {
32
+ bw._getFs().then(function(fs) {
33
+ if (!fs) { console.error('bw.saveClientFile: fs module not available'); return; }
34
+ fs.writeFile(fname, data, function(err) {
35
+ if (err) {
36
+ console.error("Error saving file:", err);
37
+ }
38
+ });
39
+ });
40
+ } else {
41
+ var blob = new Blob([data], { type: "application/octet-stream" });
42
+ var url = window.URL.createObjectURL(blob);
43
+ var a = bw.createDOM({
44
+ t: 'a',
45
+ a: {
46
+ href: url,
47
+ download: fname,
48
+ style: 'display: none'
49
+ }
50
+ });
51
+ document.body.appendChild(a);
52
+ a.click();
53
+ window.URL.revokeObjectURL(url);
54
+ document.body.removeChild(a);
55
+ }
56
+ };
57
+
58
+ /**
59
+ * Save data as a JSON file with pretty formatting.
60
+ *
61
+ * @param {string} fname - Filename to save as
62
+ * @param {*} data - Data to serialize as JSON
63
+ * @category File I/O
64
+ */
65
+ bw.saveClientJSON = function(fname, data) {
66
+ bw.saveClientFile(fname, JSON.stringify(data, null, 2));
67
+ };
68
+
69
+ /**
70
+ * Load a file by path (Node.js) or URL (browser via XHR).
71
+ *
72
+ * @param {string} fname - File path (Node) or URL (browser)
73
+ * @param {Function} callback - Called with (data, error). data is null on error.
74
+ * @param {Object} [options] - Options
75
+ * @param {string} [options.parser="raw"] - "raw" for string, "JSON" to auto-parse
76
+ * @returns {string} "BW_OK"
77
+ * @category File I/O
78
+ */
79
+ bw.loadClientFile = function(fname, callback, options) {
80
+ var opts = { parser: 'raw' };
81
+ if (options && options.parser) { opts.parser = options.parser; }
82
+ var parse = (opts.parser === 'JSON') ? JSON.parse : function(s) { return s; };
83
+
84
+ if (bw.isNodeJS()) {
85
+ bw._getFs().then(function(fs) {
86
+ if (!fs) { callback(null, new Error('fs module not available')); return; }
87
+ fs.readFile(fname, 'utf8', function(err, data) {
88
+ if (err) { callback(null, err); }
89
+ else {
90
+ try { callback(parse(data), null); }
91
+ catch (e) { callback(null, e); }
92
+ }
93
+ });
94
+ });
95
+ } else {
96
+ var x = new XMLHttpRequest();
97
+ x.open('GET', fname, true);
98
+ x.onreadystatechange = function() {
99
+ if (x.readyState === 4) {
100
+ if (x.status >= 200 && x.status < 300) {
101
+ try { callback(parse(x.responseText), null); }
102
+ catch (e) { callback(null, e); }
103
+ } else {
104
+ callback(null, new Error('HTTP ' + x.status + ': ' + fname));
105
+ }
106
+ }
107
+ };
108
+ x.send(null);
109
+ }
110
+ return 'BW_OK';
111
+ };
112
+
113
+ /**
114
+ * Load a JSON file by path (Node.js) or URL (browser).
115
+ *
116
+ * @param {string} fname - File path (Node) or URL (browser)
117
+ * @param {Function} callback - Called with (parsedData, error)
118
+ * @returns {string} "BW_OK"
119
+ * @category File I/O
120
+ */
121
+ bw.loadClientJSON = function(fname, callback) {
122
+ return bw.loadClientFile(fname, callback, { parser: 'JSON' });
123
+ };
124
+
125
+ /**
126
+ * Prompt user to pick a local file via file dialog (browser only).
127
+ *
128
+ * @param {Function} callback - Called with (data, filename, error)
129
+ * @param {Object} [options] - Options
130
+ * @param {string} [options.accept] - File type filter (e.g. ".json,.txt")
131
+ * @param {string} [options.parser="raw"] - "raw" for string, "JSON" to auto-parse
132
+ * @category File I/O
133
+ */
134
+ bw.loadLocalFile = function(callback, options) {
135
+ var opts = { parser: 'raw', accept: '' };
136
+ if (options) {
137
+ if (options.parser) { opts.parser = options.parser; }
138
+ if (options.accept) { opts.accept = options.accept; }
139
+ }
140
+ var parse = (opts.parser === 'JSON') ? JSON.parse : function(s) { return s; };
141
+
142
+ if (bw.isNodeJS()) {
143
+ callback(null, '', new Error('bw.loadLocalFile is browser-only. Use bw.loadClientFile() in Node.'));
144
+ return;
145
+ }
146
+
147
+ var input = bw.createDOM({
148
+ t: 'input',
149
+ a: {
150
+ type: 'file',
151
+ accept: opts.accept,
152
+ style: 'display: none'
153
+ }
154
+ });
155
+ input.addEventListener('change', function() {
156
+ var file = input.files[0];
157
+ if (!file) { callback(null, '', new Error('No file selected')); return; }
158
+ var reader = new FileReader();
159
+ reader.onload = function(e) {
160
+ try { callback(parse(e.target.result), file.name, null); }
161
+ catch (err) { callback(null, file.name, err); }
162
+ };
163
+ reader.onerror = function() { callback(null, file.name, reader.error); };
164
+ reader.readAsText(file);
165
+ input.remove();
166
+ });
167
+ document.body.appendChild(input);
168
+ input.click();
169
+ };
170
+
171
+ /**
172
+ * Prompt user to pick a local JSON file via file dialog (browser only).
173
+ *
174
+ * @param {Function} callback - Called with (parsedData, filename, error)
175
+ * @category File I/O
176
+ */
177
+ bw.loadLocalJSON = function(callback) {
178
+ bw.loadLocalFile(callback, { parser: 'JSON', accept: '.json' });
179
+ };
180
+ }
@@ -2,13 +2,13 @@
2
2
  * bitwrench-lean.js — Entry point for the lean build.
3
3
  *
4
4
  * This is identical to bitwrench.js but the Rollup config redirects
5
- * the bitwrench-components-v2.js import to an empty stub, so no
5
+ * the bitwrench-bccl.js import to an empty stub, so no
6
6
  * BCCL component code is included in the output.
7
7
  *
8
8
  * Includes: HTML/DOM generation, CSS generation, color utilities,
9
9
  * state management, pub/sub, file I/O, random/lorem,
10
10
  * cookies, URL params, logging, makeTable, makeDataTable.
11
- * Excludes: All make* component helpers from bitwrench-components-v2.js
11
+ * Excludes: All make* component helpers from bitwrench-bccl.js
12
12
  * (makeButton, makeCard, makeAlert, makeTabs, etc.)
13
13
  */
14
14
  export { default } from './bitwrench.js';