softui-css 1.13.0 → 1.14.0

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/dist/softui.js CHANGED
@@ -303,6 +303,21 @@ const SoftUI = (() => {
303
303
  // Drawers
304
304
  initDrawers();
305
305
 
306
+ // Editable Text
307
+ initEditable();
308
+
309
+ // Scrollspy
310
+ initScrollspy();
311
+
312
+ // Countdowns
313
+ initCountdowns();
314
+
315
+ // Segmented Controls
316
+ initSegmented();
317
+
318
+ // Navigation Menu
319
+ initNavMenu();
320
+
306
321
  // Data Tables
307
322
  initDataTables();
308
323
 
@@ -2646,6 +2661,304 @@ const SoftUI = (() => {
2646
2661
  });
2647
2662
  }
2648
2663
 
2664
+ function initEditable() {
2665
+ document.querySelectorAll('.sui-editable').forEach(function(el) {
2666
+ const valueEl = el.querySelector('.sui-editable-value');
2667
+ if (!valueEl) return;
2668
+
2669
+ el.addEventListener('click', function() {
2670
+ if (el.querySelector('.sui-editable-input')) return; // Already editing
2671
+
2672
+ const currentText = valueEl.textContent;
2673
+ const input = document.createElement('input');
2674
+ input.className = 'sui-editable-input';
2675
+ input.type = 'text';
2676
+ input.value = currentText;
2677
+ input.style.fontSize = getComputedStyle(valueEl).fontSize;
2678
+ input.style.fontWeight = getComputedStyle(valueEl).fontWeight;
2679
+
2680
+ valueEl.style.display = 'none';
2681
+ const icon = el.querySelector('.sui-editable-icon');
2682
+ if (icon) icon.style.display = 'none';
2683
+
2684
+ el.insertBefore(input, valueEl);
2685
+ input.focus();
2686
+ input.select();
2687
+
2688
+ let cancelled = false;
2689
+
2690
+ function save() {
2691
+ if (cancelled) return;
2692
+ const newVal = input.value.trim() || currentText;
2693
+ valueEl.textContent = newVal;
2694
+ valueEl.style.display = '';
2695
+ if (icon) icon.style.display = '';
2696
+ input.remove();
2697
+ el.dispatchEvent(new CustomEvent('editable:save', { detail: { value: newVal, previous: currentText } }));
2698
+ }
2699
+
2700
+ function cancel() {
2701
+ cancelled = true;
2702
+ valueEl.style.display = '';
2703
+ if (icon) icon.style.display = '';
2704
+ input.remove();
2705
+ el.dispatchEvent(new CustomEvent('editable:cancel'));
2706
+ }
2707
+
2708
+ input.addEventListener('keydown', function(e) {
2709
+ if (e.key === 'Enter') { e.preventDefault(); save(); }
2710
+ if (e.key === 'Escape') { e.preventDefault(); cancel(); }
2711
+ });
2712
+
2713
+ input.addEventListener('blur', save);
2714
+ });
2715
+ });
2716
+ }
2717
+
2718
+ function initScrollspy() {
2719
+ document.querySelectorAll('[data-sui-scrollspy]').forEach(function(nav) {
2720
+ const links = nav.querySelectorAll('a[href^="#"]');
2721
+ if (!links.length) return;
2722
+
2723
+ const targetIds = [];
2724
+ links.forEach(function(link) {
2725
+ const id = link.getAttribute('href').slice(1);
2726
+ if (id) targetIds.push(id);
2727
+ });
2728
+
2729
+ // Find scroll container — either specified or auto-detect from first target's scrollable parent
2730
+ const firstTarget = document.getElementById(targetIds[0]);
2731
+ let scrollRoot = null;
2732
+ if (firstTarget) {
2733
+ let parent = firstTarget.parentElement;
2734
+ while (parent && parent !== document.body) {
2735
+ const style = getComputedStyle(parent);
2736
+ if (style.overflowY === 'auto' || style.overflowY === 'scroll') {
2737
+ scrollRoot = parent;
2738
+ break;
2739
+ }
2740
+ parent = parent.parentElement;
2741
+ }
2742
+ }
2743
+
2744
+ const visibleSections = new Set();
2745
+ let clickLock = false;
2746
+
2747
+ const observer = new IntersectionObserver(function(entries) {
2748
+ if (clickLock) return;
2749
+ entries.forEach(function(entry) {
2750
+ if (entry.isIntersecting) {
2751
+ visibleSections.add(entry.target.id);
2752
+ } else {
2753
+ visibleSections.delete(entry.target.id);
2754
+ }
2755
+ });
2756
+ // Check if scrolled to bottom of container
2757
+ let atBottom = false;
2758
+ if (scrollRoot) {
2759
+ atBottom = scrollRoot.scrollTop + scrollRoot.clientHeight >= scrollRoot.scrollHeight - 5;
2760
+ } else {
2761
+ atBottom = window.innerHeight + window.scrollY >= document.body.scrollHeight - 5;
2762
+ }
2763
+
2764
+ if (atBottom && visibleSections.size > 0) {
2765
+ // At bottom — pick the last visible section
2766
+ for (let i = targetIds.length - 1; i >= 0; i--) {
2767
+ if (visibleSections.has(targetIds[i])) {
2768
+ links.forEach(function(l) { l.classList.remove('active'); });
2769
+ const active = nav.querySelector('a[href="#' + targetIds[i] + '"]');
2770
+ if (active) active.classList.add('active');
2771
+ break;
2772
+ }
2773
+ }
2774
+ } else {
2775
+ // Pick the first visible section in document order
2776
+ for (let i = 0; i < targetIds.length; i++) {
2777
+ if (visibleSections.has(targetIds[i])) {
2778
+ links.forEach(function(l) { l.classList.remove('active'); });
2779
+ const active = nav.querySelector('a[href="#' + targetIds[i] + '"]');
2780
+ if (active) active.classList.add('active');
2781
+ break;
2782
+ }
2783
+ }
2784
+ }
2785
+ }, {
2786
+ root: scrollRoot,
2787
+ rootMargin: '0px 0px -30% 0px',
2788
+ threshold: 0
2789
+ });
2790
+
2791
+ targetIds.forEach(function(id) {
2792
+ const el = document.getElementById(id);
2793
+ if (el) observer.observe(el);
2794
+ });
2795
+
2796
+ // Click to scroll and activate
2797
+ links.forEach(function(link) {
2798
+ link.addEventListener('click', function(e) {
2799
+ e.preventDefault();
2800
+ const id = link.getAttribute('href').slice(1);
2801
+ const el = document.getElementById(id);
2802
+ if (!el) return;
2803
+ clickLock = true;
2804
+ links.forEach(function(l) { l.classList.remove('active'); });
2805
+ link.classList.add('active');
2806
+ if (scrollRoot) {
2807
+ scrollRoot.scrollTo({ top: el.offsetTop - scrollRoot.offsetTop, behavior: 'smooth' });
2808
+ } else {
2809
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });
2810
+ }
2811
+ setTimeout(function() { clickLock = false; }, 600);
2812
+ });
2813
+ });
2814
+ });
2815
+ }
2816
+
2817
+ function initCountdowns() {
2818
+ document.querySelectorAll('.sui-countdown[data-date]').forEach(function(el) {
2819
+ const dateStr = el.getAttribute('data-date');
2820
+
2821
+ // Support relative dates: "+2y", "+30d", "+2y5d", "+6h30m"
2822
+ let target;
2823
+ if (dateStr.startsWith('+')) {
2824
+ target = new Date();
2825
+ const parts = dateStr.slice(1).matchAll(/(\d+)([ydhms])/g);
2826
+ for (const p of parts) {
2827
+ const val = parseInt(p[1], 10);
2828
+ const unit = p[2];
2829
+ if (unit === 'y') target.setFullYear(target.getFullYear() + val);
2830
+ else if (unit === 'd') target.setDate(target.getDate() + val);
2831
+ else if (unit === 'h') target.setHours(target.getHours() + val);
2832
+ else if (unit === 'm') target.setMinutes(target.getMinutes() + val);
2833
+ else if (unit === 's') target.setSeconds(target.getSeconds() + val);
2834
+ }
2835
+ target = target.getTime();
2836
+ } else {
2837
+ target = new Date(dateStr).getTime();
2838
+ }
2839
+
2840
+ const yearsEl = el.querySelector('[data-years]');
2841
+ const daysEl = el.querySelector('[data-days]');
2842
+ const hoursEl = el.querySelector('[data-hours]');
2843
+ const minsEl = el.querySelector('[data-minutes]');
2844
+ const secsEl = el.querySelector('[data-seconds]');
2845
+
2846
+ function update() {
2847
+ const now = Date.now();
2848
+ const diff = Math.max(0, target - now);
2849
+ let remaining = diff;
2850
+ const y = Math.floor(remaining / 31536000000);
2851
+ remaining %= 31536000000;
2852
+ const d = Math.floor(remaining / 86400000);
2853
+ remaining %= 86400000;
2854
+ const h = Math.floor(remaining / 3600000);
2855
+ remaining %= 3600000;
2856
+ const m = Math.floor(remaining / 60000);
2857
+ remaining %= 60000;
2858
+ const s = Math.floor(remaining / 1000);
2859
+ if (yearsEl) yearsEl.textContent = String(y).padStart(2, '0');
2860
+ if (daysEl) daysEl.textContent = String(d).padStart(2, '0');
2861
+ if (hoursEl) hoursEl.textContent = String(h).padStart(2, '0');
2862
+ if (minsEl) minsEl.textContent = String(m).padStart(2, '0');
2863
+ if (secsEl) secsEl.textContent = String(s).padStart(2, '0');
2864
+ if (diff === 0) {
2865
+ clearInterval(timer);
2866
+ el.dispatchEvent(new Event('countdown:end'));
2867
+ }
2868
+ }
2869
+
2870
+ update();
2871
+ const timer = setInterval(update, 1000);
2872
+ });
2873
+ }
2874
+
2875
+ function initSegmented() {
2876
+ document.querySelectorAll('.sui-segmented').forEach(function(seg) {
2877
+ const indicator = seg.querySelector('.sui-segmented-indicator');
2878
+ if (!indicator) return;
2879
+
2880
+ function updateIndicator() {
2881
+ const checked = seg.querySelector('input:checked');
2882
+ if (!checked) return;
2883
+ const label = checked.nextElementSibling;
2884
+ if (!label) return;
2885
+ indicator.style.left = label.offsetLeft + 'px';
2886
+ indicator.style.width = label.offsetWidth + 'px';
2887
+ }
2888
+
2889
+ // Initial position
2890
+ updateIndicator();
2891
+
2892
+ // Listen for changes
2893
+ seg.querySelectorAll('input').forEach(function(input) {
2894
+ input.addEventListener('change', updateIndicator);
2895
+ });
2896
+
2897
+ // Recalculate on resize
2898
+ window.addEventListener('resize', updateIndicator);
2899
+ });
2900
+ }
2901
+
2902
+ function initNavMenu() {
2903
+ // Toggle on click
2904
+ document.addEventListener('click', function(e) {
2905
+ if (!e.target.closest) return;
2906
+ const trigger = e.target.closest('.sui-nav-menu-trigger');
2907
+ if (trigger && !trigger.hasAttribute('href')) {
2908
+ const item = trigger.closest('.sui-nav-menu-item');
2909
+ if (!item) return;
2910
+ // Close other open items
2911
+ document.querySelectorAll('.sui-nav-menu-item.open').forEach(function(i) {
2912
+ if (i !== item) i.classList.remove('open');
2913
+ });
2914
+ item.classList.toggle('open');
2915
+ e.stopPropagation();
2916
+ return;
2917
+ }
2918
+ // Click on sub-menu trigger (click mode)
2919
+ const subLink = e.target.closest('.sui-nav-menu-sub > .sui-nav-menu-link');
2920
+ if (subLink) {
2921
+ const sub = subLink.closest('.sui-nav-menu-sub');
2922
+ const panel = sub.closest('.sui-nav-menu-panel');
2923
+ if (panel && panel.closest('.sui-nav-menu-sub-click')) {
2924
+ e.preventDefault();
2925
+ e.stopPropagation();
2926
+ // Close sibling subs
2927
+ panel.querySelectorAll('.sui-nav-menu-sub.open').forEach(function(s) {
2928
+ if (s !== sub) s.classList.remove('open');
2929
+ });
2930
+ sub.classList.toggle('open');
2931
+ return;
2932
+ }
2933
+ }
2934
+
2935
+ // Click on a nav-menu link closes everything
2936
+ const link = e.target.closest('.sui-nav-menu-link');
2937
+ if (link && link.closest('.sui-nav-menu-item')) {
2938
+ document.querySelectorAll('.sui-nav-menu-item.open').forEach(function(i) {
2939
+ i.classList.remove('open');
2940
+ });
2941
+ return;
2942
+ }
2943
+
2944
+ // Click outside closes all
2945
+ if (!e.target.closest('.sui-nav-menu-item')) {
2946
+ document.querySelectorAll('.sui-nav-menu-item.open').forEach(function(i) {
2947
+ i.classList.remove('open');
2948
+ });
2949
+ }
2950
+ });
2951
+
2952
+ // Escape closes
2953
+ document.addEventListener('keydown', function(e) {
2954
+ if (e.key === 'Escape') {
2955
+ document.querySelectorAll('.sui-nav-menu-item.open').forEach(function(i) {
2956
+ i.classList.remove('open');
2957
+ });
2958
+ }
2959
+ });
2960
+ }
2961
+
2649
2962
  function initDrawers() {
2650
2963
  document.querySelectorAll('.sui-drawer').forEach(function(backdrop) {
2651
2964
  const panel = backdrop.querySelector('.sui-sheet-bottom');