@khipu/design-system 0.2.0-alpha.13 → 0.2.0-alpha.131

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.
@@ -892,11 +892,87 @@ const ui = _context.ui;
892
892
  initCountdown();
893
893
  initSegmentedTabs();
894
894
  initStickyInvoice();
895
+ initHideOnScroll();
895
896
  initBankModal();
897
+ initMerchantLogoBackdrop();
896
898
 
897
899
  console.log('Material Design initialization complete!');
898
900
  }
899
901
 
902
+ /**
903
+ * Initialize merchant logo backdrop (KTUF-204)
904
+ * Los logos de comercio van sobre el fondo neutro, no sobre el color de marca (los PNG
905
+ * con transparencia se ahogan contra su propio color). La luminancia decide entre los
906
+ * dos neutros: paper para logos oscuros, gray-800 para logos claros. Espejo vanilla de
907
+ * KdsInvoiceMerchant/logoLuminance.ts para las vistas server-side (payment).
908
+ * @param {Element} root - Root element to scope the query (default: document)
909
+ */
910
+ function initMerchantLogoBackdrop(root) {
911
+ root = root || document;
912
+ var LIGHT_LOGO_LUMINANCE_THRESHOLD = 0.72;
913
+ var SAMPLE_SIZE = 16;
914
+ var ALPHA_THRESHOLD = 128;
915
+ var MIN_OPAQUE_RATIO = 0.02;
916
+
917
+ root.querySelectorAll('.kds-invoice-merchant').forEach(function(tile) {
918
+ var img = tile.querySelector('img');
919
+ if (!img) return;
920
+
921
+ /* Con logo: fondo neutro; el color de marca queda solo para el fallback. */
922
+ var brandBackground = tile.style.background;
923
+ tile.style.background = '';
924
+ tile.classList.add('kds-invoice-merchant-neutral');
925
+
926
+ /* Si el logo falla (el onerror server-side lo reemplaza por el ícono),
927
+ restaurar el tile de color de marca. */
928
+ img.addEventListener('error', function() {
929
+ tile.classList.remove('kds-invoice-merchant-neutral', 'dark');
930
+ tile.style.background = brandBackground;
931
+ });
932
+
933
+ measureLogoLuminance(img.getAttribute('src'), function(luminance) {
934
+ if (luminance != null && luminance > LIGHT_LOGO_LUMINANCE_THRESHOLD &&
935
+ tile.classList.contains('kds-invoice-merchant-neutral')) {
936
+ tile.classList.add('dark');
937
+ }
938
+ });
939
+ });
940
+
941
+ function measureLogoLuminance(url, callback) {
942
+ if (!url) { callback(null); return; }
943
+ var probe = new Image();
944
+ probe.crossOrigin = 'anonymous';
945
+ probe.onload = function() {
946
+ try {
947
+ var canvas = document.createElement('canvas');
948
+ canvas.width = SAMPLE_SIZE;
949
+ canvas.height = SAMPLE_SIZE;
950
+ var context = canvas.getContext('2d');
951
+ if (!context) { callback(null); return; }
952
+ context.drawImage(probe, 0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
953
+ var data = context.getImageData(0, 0, SAMPLE_SIZE, SAMPLE_SIZE).data;
954
+ var sum = 0;
955
+ var opaquePixels = 0;
956
+ for (var i = 0; i < data.length; i += 4) {
957
+ if (data[i + 3] < ALPHA_THRESHOLD) continue;
958
+ sum += (0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2]) / 255;
959
+ opaquePixels++;
960
+ }
961
+ if (opaquePixels < SAMPLE_SIZE * SAMPLE_SIZE * MIN_OPAQUE_RATIO) {
962
+ callback(null);
963
+ return;
964
+ }
965
+ callback(sum / opaquePixels);
966
+ } catch (e) {
967
+ /* canvas tainted: el origen no permite CORS — no medible */
968
+ callback(null);
969
+ }
970
+ };
971
+ probe.onerror = function() { callback(null); };
972
+ probe.src = url;
973
+ }
974
+ }
975
+
900
976
  /**
901
977
  * Initialize sidenav for mobile navigation
902
978
  */
@@ -1051,8 +1127,12 @@ const ui = _context.ui;
1051
1127
  if (panel) {
1052
1128
  if (expanded) {
1053
1129
  panel.classList.remove('open');
1130
+ // Clear the inline cap so the CSS collapse rule (max-height: 0) animates the close.
1131
+ panel.style.maxHeight = '';
1054
1132
  } else {
1055
1133
  panel.classList.add('open');
1134
+ // Size to the content so the expand fits any length instead of clipping a fixed cap.
1135
+ panel.style.maxHeight = panel.scrollHeight + 'px';
1056
1136
  }
1057
1137
  }
1058
1138
  });
@@ -1330,9 +1410,9 @@ const ui = _context.ui;
1330
1410
  function initStickyInvoice(root) {
1331
1411
  root = root || document;
1332
1412
 
1333
- // Progressive collapse range: 0px (expanded) to 30px (collapsed)
1413
+ // Progressive collapse range: 0px (expanded) to 20px (collapsed)
1334
1414
  var COLLAPSE_START = 0;
1335
- var COLLAPSE_END = 30;
1415
+ var COLLAPSE_END = 20;
1336
1416
 
1337
1417
  var lastScrollY = 0;
1338
1418
  var ticking = false;
@@ -1380,13 +1460,16 @@ const ui = _context.ui;
1380
1460
  if (!currentScreen.style.getPropertyValue('--collapse-collapsible-h')) {
1381
1461
  var collapsible = currentSticky.querySelector('.kds-invoice-collapsible');
1382
1462
  if (collapsible) {
1383
- currentScreen.style.setProperty('--collapse-collapsible-h', Math.min(collapsible.offsetHeight, 87) + 'px');
1463
+ currentScreen.style.setProperty('--collapse-collapsible-h', collapsible.offsetHeight + 'px');
1384
1464
  }
1385
1465
  }
1386
1466
 
1387
1467
  // Single DOM write per frame — set on screen (parent) so it cascades to sticky + siblings
1388
1468
  currentScreen.style.setProperty('--collapse-progress', progress);
1389
1469
 
1470
+ // Toggle collapsed state class for discrete styling that can't interpolate (e.g. align-items)
1471
+ currentSticky.classList.toggle('is-collapsed', progress >= 1);
1472
+
1390
1473
  // Close expand panels as soon as the sticky header starts collapsing
1391
1474
  if (progress > 0) {
1392
1475
  currentSticky.querySelectorAll('[data-expand-toggle][aria-expanded="true"]').forEach(function(toggle) {
@@ -1410,6 +1493,9 @@ const ui = _context.ui;
1410
1493
  screen.style.removeProperty('--collapse-progress');
1411
1494
  screen.style.removeProperty('--collapse-collapsible-h');
1412
1495
  });
1496
+ root.querySelectorAll('.kds-invoice-sticky.is-collapsed').forEach(function(el) {
1497
+ el.classList.remove('is-collapsed');
1498
+ });
1413
1499
  }
1414
1500
  });
1415
1501
 
@@ -1419,6 +1505,67 @@ const ui = _context.ui;
1419
1505
  onScroll();
1420
1506
  }
1421
1507
 
1508
+ /**
1509
+ * Initialize hide-on-scroll for floating elements (vanilla equivalent of the React
1510
+ * `useHideOnScroll` hook). Any element with `[data-hide-on-scroll]` gets its hide class
1511
+ * (default `kds-fab--hidden`, override with `data-hide-class`) toggled: hidden when
1512
+ * scrolling down past the threshold, shown when scrolling up, always shown within
1513
+ * `data-hide-top-offset` px of the top. Tune with `data-hide-threshold` (default 8).
1514
+ *
1515
+ * Works standalone (`window.scrollY`) and embedded in an iframe, where the parent posts
1516
+ * `postMessage({ type: 'VIEWPORT_OFFSET', offsetTop })`.
1517
+ * @param {Element} root - Root element to scope the query (default: document)
1518
+ */
1519
+ function initHideOnScroll(root) {
1520
+ root = root || document;
1521
+ var els = root.querySelectorAll('[data-hide-on-scroll]');
1522
+ if (!els.length) return;
1523
+
1524
+ var viewportOffset = 0;
1525
+ var ticking = false;
1526
+ var lastYs = new Map();
1527
+
1528
+ function currentY() {
1529
+ return Math.max(window.scrollY || window.pageYOffset || 0, viewportOffset);
1530
+ }
1531
+
1532
+ function apply() {
1533
+ ticking = false;
1534
+ var y = currentY();
1535
+ els.forEach(function(el) {
1536
+ var threshold = parseInt(el.getAttribute('data-hide-threshold'), 10) || 8;
1537
+ var topOffset = parseInt(el.getAttribute('data-hide-top-offset'), 10) || 0;
1538
+ var hideClass = el.getAttribute('data-hide-class') || 'kds-fab--hidden';
1539
+ var lastY = lastYs.has(el) ? lastYs.get(el) : y;
1540
+ if (y <= topOffset) {
1541
+ el.classList.remove(hideClass);
1542
+ lastYs.set(el, y);
1543
+ return;
1544
+ }
1545
+ var delta = y - lastY;
1546
+ if (Math.abs(delta) < threshold) return;
1547
+ el.classList.toggle(hideClass, delta > 0);
1548
+ lastYs.set(el, y);
1549
+ });
1550
+ }
1551
+
1552
+ function onScroll() {
1553
+ if (ticking) return;
1554
+ ticking = true;
1555
+ requestAnimationFrame(apply);
1556
+ }
1557
+
1558
+ window.addEventListener('scroll', onScroll, { passive: true });
1559
+ window.addEventListener('resize', onScroll);
1560
+ window.addEventListener('message', function(event) {
1561
+ if (event.data && event.data.type === 'VIEWPORT_OFFSET') {
1562
+ viewportOffset = Math.max(0, event.data.offsetTop || 0);
1563
+ onScroll();
1564
+ }
1565
+ });
1566
+ apply();
1567
+ }
1568
+
1422
1569
  /**
1423
1570
  * Initialize copyable table rows
1424
1571
  * Delegated click on .kds-copyable-table-row[data-copy] copies value and shows feedback
@@ -1461,7 +1608,7 @@ const ui = _context.ui;
1461
1608
  var rows = container.querySelectorAll('.kds-copyable-table-row[data-copy]');
1462
1609
  var values = [];
1463
1610
  rows.forEach(function(r) {
1464
- var key = r.querySelector('.k');
1611
+ var key = r.querySelector('.kds-key');
1465
1612
  var val = r.dataset.copy;
1466
1613
  if (key) {
1467
1614
  values.push(key.textContent.trim() + ': ' + val);
@@ -1589,6 +1736,7 @@ const ui = _context.ui;
1589
1736
  window.Khipu.initInfoTip = initInfoTip;
1590
1737
  window.Khipu.initBankModal = initBankModal;
1591
1738
  window.Khipu.initStickyInvoice = initStickyInvoice;
1739
+ window.Khipu.initHideOnScroll = initHideOnScroll;
1592
1740
 
1593
1741
  // Also export showSnackbar to global scope for backward compatibility
1594
1742
  window.showSnackbar = showSnackbar;