bitwrench 2.0.13 → 2.0.15

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 (46) hide show
  1. package/README.md +4 -4
  2. package/dist/bitwrench-code-edit.cjs.js +46 -46
  3. package/dist/bitwrench-code-edit.cjs.min.js +16 -0
  4. package/dist/bitwrench-code-edit.es5.js +8 -8
  5. package/dist/bitwrench-code-edit.es5.min.js +2 -2
  6. package/dist/bitwrench-code-edit.esm.js +46 -46
  7. package/dist/bitwrench-code-edit.esm.min.js +2 -2
  8. package/dist/bitwrench-code-edit.umd.js +46 -46
  9. package/dist/bitwrench-code-edit.umd.min.js +2 -2
  10. package/dist/bitwrench-lean.cjs.js +5011 -3419
  11. package/dist/bitwrench-lean.cjs.min.js +35 -6
  12. package/dist/bitwrench-lean.es5.js +6218 -4272
  13. package/dist/bitwrench-lean.es5.min.js +32 -3
  14. package/dist/bitwrench-lean.esm.js +5011 -3419
  15. package/dist/bitwrench-lean.esm.min.js +35 -6
  16. package/dist/bitwrench-lean.umd.js +5011 -3419
  17. package/dist/bitwrench-lean.umd.min.js +35 -6
  18. package/dist/bitwrench.cjs.js +6966 -4662
  19. package/dist/bitwrench.cjs.min.js +38 -8
  20. package/dist/bitwrench.css +2453 -4784
  21. package/dist/bitwrench.es5.js +9592 -6813
  22. package/dist/bitwrench.es5.min.js +34 -5
  23. package/dist/bitwrench.esm.js +6966 -4662
  24. package/dist/bitwrench.esm.min.js +38 -8
  25. package/dist/bitwrench.min.css +1 -0
  26. package/dist/bitwrench.umd.js +6966 -4662
  27. package/dist/bitwrench.umd.min.js +38 -8
  28. package/dist/builds.json +89 -67
  29. package/dist/sri.json +28 -26
  30. package/package.json +7 -5
  31. package/readme.html +14 -14
  32. package/src/{bitwrench-components-v2.js → bitwrench-bccl.js} +1311 -600
  33. package/src/bitwrench-code-edit.js +45 -45
  34. package/src/bitwrench-color-utils.js +154 -27
  35. package/src/bitwrench-components-stub.js +4 -1
  36. package/src/bitwrench-file-ops.js +180 -0
  37. package/src/bitwrench-lean.js +2 -2
  38. package/src/bitwrench-styles.js +1468 -3494
  39. package/src/bitwrench-utils.js +458 -0
  40. package/src/bitwrench.js +1795 -1349
  41. package/src/cli/layout-default.js +18 -18
  42. package/src/generate-css.js +73 -53
  43. package/src/version.js +3 -3
  44. package/src/bitwrench-component-base.js +0 -736
  45. package/src/bitwrench-components-inline.js +0 -374
  46. package/src/bitwrench-components.js +0 -610
@@ -14,53 +14,53 @@
14
14
  // -- CSS (injected once) ----------------------------------------------
15
15
  var _cssInjected = false;
16
16
  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}' +
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}' +
38
38
  // 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}';
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}';
58
58
 
59
59
  function ensureCSS(bw) {
60
60
  if (_cssInjected) return;
61
61
  _cssInjected = true;
62
62
  if (bw && bw.injectCSS) {
63
- bw.injectCSS(CSS_TEXT, { id: 'bw-code-edit-styles' });
63
+ bw.injectCSS(CSS_TEXT, { id: 'bw_code_edit_styles' });
64
64
  }
65
65
  }
66
66
 
@@ -483,7 +483,7 @@ function tokensToTACO(tokArr) {
483
483
  if (tok.type === 'plain') {
484
484
  result.push(tok.text);
485
485
  } else {
486
- result.push({ t: 'span', a: { class: 'bw-ce-' + tok.type }, c: tok.text });
486
+ result.push({ t: 'span', a: { class: 'bw_ce_' + tok.type }, c: tok.text });
487
487
  }
488
488
  }
489
489
  return result;
@@ -537,13 +537,13 @@ function codeEditor(opts) {
537
537
  var lang = opts.lang || 'js';
538
538
  var height = opts.height || '180px';
539
539
  var readOnly = !!opts.readOnly;
540
- var className = 'bw-ce' + (opts.className ? ' ' + opts.className : '');
540
+ var className = 'bw_ce' + (opts.className ? ' ' + opts.className : '');
541
541
 
542
542
  var highlighted = highlight(code, lang);
543
543
 
544
544
  var codeAttrs = {
545
545
  spellcheck: 'false',
546
- class: 'bw-ce-code'
546
+ class: 'bw_ce_code'
547
547
  };
548
548
  if (!readOnly) {
549
549
  codeAttrs.contenteditable = 'true';
@@ -557,7 +557,7 @@ function codeEditor(opts) {
557
557
  ],
558
558
  o: {
559
559
  mounted: function(el) {
560
- var codeEl = el.querySelector('.bw-ce-code');
560
+ var codeEl = el.querySelector('.bw_ce_code');
561
561
  if (!codeEl) return;
562
562
 
563
563
  var currentCode = code;
@@ -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
  /**
@@ -261,6 +259,29 @@ export function textOnColor(hex) {
261
259
  return relativeLuminance(hex) > 0.179 ? '#000' : '#fff';
262
260
  }
263
261
 
262
+ /**
263
+ * Shift a color's hue toward a target hue by a given amount.
264
+ * Uses shortest-arc interpolation on the hue wheel.
265
+ * @param {string} sourceHex - Color to shift
266
+ * @param {string} targetHex - Color whose hue to shift toward
267
+ * @param {number} [amount=0.20] - 0 = no shift, 1 = full shift to target hue
268
+ * @returns {string} Harmonized hex color
269
+ */
270
+ export function harmonize(sourceHex, targetHex, amount) {
271
+ if (amount === undefined) amount = 0.20;
272
+ if (amount === 0) return sourceHex;
273
+ var srcHsl = hexToHsl(sourceHex);
274
+ var tgtHsl = hexToHsl(targetHex);
275
+
276
+ // Shortest-arc hue interpolation
277
+ var diff = tgtHsl[0] - srcHsl[0];
278
+ if (diff > 180) diff -= 360;
279
+ if (diff < -180) diff += 360;
280
+
281
+ var newHue = (srcHsl[0] + diff * amount + 360) % 360;
282
+ return hslToHex([newHue, srcHsl[1], srcHsl[2]]);
283
+ }
284
+
264
285
  /**
265
286
  * Derive a full shade palette for a single semantic color.
266
287
  * @param {string} hex - Base color hex
@@ -280,31 +301,137 @@ export function deriveShades(hex) {
280
301
  };
281
302
  }
282
303
 
304
+ /**
305
+ * Derive the alternate (luminance-inverted) version of a single seed color.
306
+ * Preserves hue, mirrors lightness, adjusts saturation for readability.
307
+ * @param {string} hex - Seed hex color
308
+ * @returns {string} Alternate hex color
309
+ */
310
+ export function deriveAlternateSeed(hex) {
311
+ var hsl = hexToHsl(hex);
312
+ var h = hsl[0], s = hsl[1], l = hsl[2];
313
+ var altL, altS;
314
+
315
+ if (l > 50) {
316
+ // Light color → make dark. Map 50-100 → 30-10 range
317
+ altL = clip(100 - l - 10, 8, 40);
318
+ // Reduce saturation slightly — vivid colors at low lightness look garish
319
+ altS = clip(s * 0.85, 0, 100);
320
+ } else {
321
+ // Dark color → make light. Map 0-50 → 65-92 range
322
+ altL = clip(100 - l + 10, 60, 92);
323
+ // Slightly increase saturation for light variant
324
+ altS = clip(s * 1.1, 0, 100);
325
+ }
326
+
327
+ return hslToHex([h, altS, altL]);
328
+ }
329
+
330
+ /**
331
+ * Determine whether a palette config is "light-flavored" based on
332
+ * the average luminance of its seed colors.
333
+ * @param {Object} config - Theme config with primary, secondary hex colors
334
+ * @returns {boolean} true if the seeds are predominantly light
335
+ */
336
+ export function isLightPalette(config) {
337
+ var lum = relativeLuminance(config.primary);
338
+ if (config.secondary) lum = (lum + relativeLuminance(config.secondary)) / 2;
339
+ if (config.tertiary) lum = (lum * 2 + relativeLuminance(config.tertiary)) / 3;
340
+ return lum > 0.179;
341
+ }
342
+
343
+ /**
344
+ * Derive a complete alternate config from a primary theme config.
345
+ * Each seed color is luminance-inverted; semantic colors are adjusted for
346
+ * the new luminance context.
347
+ * @param {Object} config - Primary theme config
348
+ * @returns {Object} Alternate theme config (same shape, inverted lightness)
349
+ */
350
+ export function deriveAlternateConfig(config) {
351
+ var alt = {};
352
+ // Invert the user's seed colors
353
+ alt.primary = deriveAlternateSeed(config.primary);
354
+ alt.secondary = deriveAlternateSeed(config.secondary);
355
+ alt.tertiary = config.tertiary ? deriveAlternateSeed(config.tertiary) : alt.primary;
356
+
357
+ // Derive alternate surface colors from primary hue
358
+ var priHsl = hexToHsl(config.primary);
359
+ var h = priHsl[0];
360
+ var isLight = isLightPalette(config);
361
+
362
+ if (isLight) {
363
+ // Primary is light → alternate needs dark surfaces
364
+ alt.light = hslToHex([h, Math.min(priHsl[1], 15), 15]);
365
+ alt.dark = hslToHex([h, 5, 88]);
366
+ } else {
367
+ // Primary is dark → alternate needs light surfaces
368
+ alt.light = hslToHex([h, Math.min(priHsl[1], 10), 96]);
369
+ alt.dark = hslToHex([h, 10, 18]);
370
+ }
371
+
372
+ // Semantic colors: harmonize toward primary, then invert for alternate
373
+ var amt = config.harmonize !== undefined ? config.harmonize : 0.20;
374
+ var semanticDefaults = {
375
+ success: '#198754', danger: '#dc3545',
376
+ warning: '#f0ad4e', info: '#17a2b8'
377
+ };
378
+ var semantics = ['success', 'danger', 'warning', 'info'];
379
+ for (var i = 0; i < semantics.length; i++) {
380
+ var key = semantics[i];
381
+ var seed = config[key] || semanticDefaults[key];
382
+ var harmonized = harmonize(seed, config.primary, amt);
383
+ alt[key] = deriveAlternateSeed(harmonized);
384
+ }
385
+
386
+ // Semantic colors are already harmonized+inverted — don't re-harmonize in derivePalette
387
+ alt.harmonize = 0;
388
+
389
+ return alt;
390
+ }
391
+
283
392
  /**
284
393
  * Derive complete palette from a theme config object.
394
+ * Semantic colors are harmonized toward the primary hue (configurable).
395
+ * Light/dark surface colors are tinted with the primary hue.
285
396
  * @param {Object} config - Theme config with primary, secondary, tertiary, etc.
286
- * @returns {Object} Full palette with shades for all 8 semantic colors + tertiary
397
+ * @param {number} [config.harmonize=0.20] - Hue shift amount for semantic colors (0-1)
398
+ * @returns {Object} Full palette with shades for all 9 semantic colors
287
399
  */
288
400
  export function derivePalette(config) {
289
- var defaults = {
290
- success: '#198754',
291
- danger: '#dc3545',
292
- warning: '#ffc107',
293
- info: '#0dcaf0',
294
- light: '#f8f9fa',
295
- dark: '#212529'
296
- };
401
+ var amt = config.harmonize !== undefined ? config.harmonize : 0.20;
402
+ var pri = config.primary;
403
+ var priHsl = hexToHsl(pri);
404
+ var h = priHsl[0];
405
+
406
+ // Semantic defaults — harmonized toward primary hue
407
+ var successBase = harmonize(config.success || '#198754', pri, amt);
408
+ var dangerBase = harmonize(config.danger || '#dc3545', pri, amt);
409
+ var warningBase = harmonize(config.warning || '#f0ad4e', pri, amt);
410
+ var infoBase = harmonize(config.info || '#17a2b8', pri, amt);
411
+
412
+ // Light/dark: derive from primary hue with low saturation (if not user-supplied)
413
+ var lightBase = config.light || hslToHex([h, 8, 97]);
414
+ var darkBase = config.dark || hslToHex([h, 10, 13]);
415
+
416
+ // Background & surface tokens — default to light (white/near-white).
417
+ // Dark backgrounds require explicit config.background / config.surface.
418
+ // Primary/secondary colors are accents, not page backgrounds, so
419
+ // isLightPalette should NOT drive bg/surface defaults.
420
+ var bgBase = config.background || '#ffffff';
421
+ var surfBase = config.surface || '#f8f9fa';
297
422
 
298
423
  var palette = {
299
- primary: deriveShades(config.primary),
300
- secondary: deriveShades(config.secondary),
301
- tertiary: deriveShades(config.tertiary),
302
- success: deriveShades(config.success || defaults.success),
303
- danger: deriveShades(config.danger || defaults.danger),
304
- warning: deriveShades(config.warning || defaults.warning),
305
- info: deriveShades(config.info || defaults.info),
306
- light: deriveShades(config.light || defaults.light),
307
- dark: deriveShades(config.dark || defaults.dark)
424
+ primary: deriveShades(config.primary),
425
+ secondary: deriveShades(config.secondary),
426
+ tertiary: deriveShades(config.tertiary),
427
+ success: deriveShades(successBase),
428
+ danger: deriveShades(dangerBase),
429
+ warning: deriveShades(warningBase),
430
+ info: deriveShades(infoBase),
431
+ light: deriveShades(lightBase),
432
+ dark: deriveShades(darkBase),
433
+ background: bgBase,
434
+ surface: surfBase
308
435
  };
309
436
 
310
437
  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';