barua-ui 0.4.1 → 0.5.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.
@@ -522,6 +522,11 @@
522
522
  }
523
523
 
524
524
  /* Dropdown wrapper (CSS-only open via <details>) */
525
+ /* A menu used as a listbox can hold a long list — twenty blocks of a serial,
526
+ every region — so it scrolls inside itself rather than running off the
527
+ card or the screen. */
528
+ .b-menu[role="listbox"] { max-block-size: min(18rem, 50dvh); overflow-y: auto; overscroll-behavior: contain; }
529
+
525
530
  .b-dropdown { position: relative; display: inline-block; }
526
531
  .b-dropdown > summary { list-style: none; cursor: pointer; }
527
532
  .b-dropdown > summary::-webkit-details-marker { display: none; }
package/css/utilities.css CHANGED
@@ -599,4 +599,61 @@
599
599
  @media (min-width: 768px) {
600
600
  .b-hide-desktop { display: none !important; }
601
601
  }
602
+
603
+ /* ---- Scroll: what a container's own position can drive ------------------
604
+ A container carrying data-b-scroll-geometry publishes --b-scroll-progress
605
+ (0–1) and --b-scroll-y, so a progress line or a parallax can be pure CSS.
606
+ .b-scroll-progress is that line: a hairline at the top of a scroll area
607
+ whose width is the distance scrolled. */
608
+ .b-scroll-progress {
609
+ position: sticky;
610
+ inset-block-start: 0;
611
+ z-index: var(--b-z-raised);
612
+ height: 2px;
613
+ width: calc(var(--b-scroll-progress, 0) * 100%);
614
+ background: var(--b-color-accent);
615
+ transition: width 80ms linear;
616
+ }
617
+
618
+ /* A large title that condenses as its scroll area moves — SwiftUI's
619
+ navigation title, on the scroll timeline so the browser drives it. Put it
620
+ on .b-large-title or .b-mobile-header__large-title inside the scroller. */
621
+ @supports (animation-timeline: scroll()) {
622
+ .b-scroll-condense {
623
+ animation: b-condense linear both;
624
+ animation-timeline: scroll(nearest);
625
+ animation-range: 0 5rem;
626
+ transform-origin: 0 100%;
627
+ }
628
+ @keyframes b-condense {
629
+ to { scale: 0.62; opacity: 0.9; }
630
+ }
631
+ }
632
+
633
+ /* A bar over content that has moved gains its edge. */
634
+ .is-scrolled > .b-mobile-header,
635
+ .is-scrolled > .b-topnav,
636
+ .is-scrolled > .b-marketing-nav,
637
+ .b-mobile-header.is-scrolled,
638
+ .b-topnav.is-scrolled,
639
+ .b-marketing-nav.is-scrolled {
640
+ box-shadow: var(--b-shadow-sm);
641
+ }
642
+
643
+ /* data-b-visible: the entrance for browsers without a view timeline, and a
644
+ hook any element can use — .is-visible arrives when the threshold does. */
645
+ @supports not (animation-timeline: view()) {
646
+ .b-reveal[data-b-visible]:not(.is-visible) { opacity: 0; translate: 0 12px; }
647
+ .b-reveal[data-b-visible] { transition: opacity var(--b-duration-slow) var(--b-ease-out), translate var(--b-duration-slow) var(--b-ease-out); }
648
+ }
649
+
650
+ /* A backplate: glass put on any one element — SwiftUI's glassBackgroundEffect.
651
+ Blur behind, a lit top edge, a soft shadow, the system radius. */
652
+ .b-backplate {
653
+ background: var(--b-material-regular-bg);
654
+ -webkit-backdrop-filter: var(--b-glass);
655
+ backdrop-filter: var(--b-glass);
656
+ border-radius: var(--b-radius-xl);
657
+ box-shadow: var(--b-glass-edge), var(--b-shadow-md);
658
+ }
602
659
  }
package/dist/index.cjs CHANGED
@@ -314,9 +314,14 @@ __export(index_exports, {
314
314
  block: () => block,
315
315
  buttonClasses: () => buttonClasses,
316
316
  cn: () => cn,
317
+ haptic: () => haptic,
317
318
  iconNames: () => iconNames,
318
319
  useBaruaTheme: () => useBaruaTheme,
319
- useToast: () => useToast
320
+ usePersistedState: () => usePersistedState,
321
+ useScrollGeometry: () => useScrollGeometry,
322
+ useSnapPosition: () => useSnapPosition,
323
+ useToast: () => useToast,
324
+ useVisible: () => useVisible
320
325
  });
321
326
  module.exports = __toCommonJS(index_exports);
322
327
 
@@ -2617,6 +2622,194 @@ var ContextMenuTarget = (0, import_react21.forwardRef)(
2617
2622
  return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(Component, { ref, "data-b-contextmenu": menu, ...rest, children });
2618
2623
  }
2619
2624
  );
2625
+
2626
+ // src/scroll.tsx
2627
+ var import_react22 = require("react");
2628
+ function readGeometry(el) {
2629
+ const page = !el || el === document.documentElement || el === document.body;
2630
+ const box = page ? document.documentElement : el;
2631
+ const x = page ? window.scrollX : el.scrollLeft;
2632
+ const y = page ? window.scrollY : el.scrollTop;
2633
+ const width = box.clientWidth, height = box.clientHeight;
2634
+ const contentWidth = box.scrollWidth, contentHeight = box.scrollHeight;
2635
+ return {
2636
+ x,
2637
+ y,
2638
+ width,
2639
+ height,
2640
+ contentWidth,
2641
+ contentHeight,
2642
+ progressX: contentWidth > width ? Math.min(1, x / (contentWidth - width)) : 0,
2643
+ progressY: contentHeight > height ? Math.min(1, y / (contentHeight - height)) : 0
2644
+ };
2645
+ }
2646
+ function useScrollGeometry(ref, onChange) {
2647
+ const [geometry, setGeometry] = (0, import_react22.useState)(null);
2648
+ (0, import_react22.useEffect)(() => {
2649
+ const el = ref?.current ?? null;
2650
+ const page = !el || el === document.documentElement || el === document.body;
2651
+ const target = page ? window : el;
2652
+ let queued = false;
2653
+ const flush = () => {
2654
+ queued = false;
2655
+ const g = readGeometry(el);
2656
+ setGeometry(g);
2657
+ onChange?.(g);
2658
+ };
2659
+ const on = () => {
2660
+ if (!queued) {
2661
+ queued = true;
2662
+ requestAnimationFrame(flush);
2663
+ }
2664
+ };
2665
+ target.addEventListener("scroll", on, { passive: true });
2666
+ const ro = !page && "ResizeObserver" in window ? new ResizeObserver(on) : null;
2667
+ if (ro && el) ro.observe(el);
2668
+ else window.addEventListener("resize", on);
2669
+ flush();
2670
+ return () => {
2671
+ target.removeEventListener("scroll", on);
2672
+ if (ro) ro.disconnect();
2673
+ else window.removeEventListener("resize", on);
2674
+ };
2675
+ }, [ref?.current]);
2676
+ return geometry;
2677
+ }
2678
+ function useVisible(ref, { threshold = 0.5 } = {}) {
2679
+ const [visible, setVisible] = (0, import_react22.useState)(false);
2680
+ (0, import_react22.useEffect)(() => {
2681
+ const el = ref.current;
2682
+ if (!el) return;
2683
+ if (!("IntersectionObserver" in window)) {
2684
+ setVisible(true);
2685
+ return;
2686
+ }
2687
+ const io = new IntersectionObserver(
2688
+ (entries) => {
2689
+ for (const entry of entries) {
2690
+ setVisible(threshold === 0 ? entry.isIntersecting : entry.intersectionRatio >= threshold);
2691
+ }
2692
+ },
2693
+ { threshold: threshold === 0 ? [0] : [0, threshold] }
2694
+ );
2695
+ io.observe(el);
2696
+ return () => io.disconnect();
2697
+ }, [ref.current, threshold]);
2698
+ return visible;
2699
+ }
2700
+ function useSnapPosition(ref) {
2701
+ const [current, setCurrent] = (0, import_react22.useState)({ id: null, index: -1 });
2702
+ const items = (0, import_react22.useCallback)(() => ref.current ? Array.from(ref.current.children).filter((c) => !c.hidden) : [], [ref]);
2703
+ (0, import_react22.useEffect)(() => {
2704
+ const box = ref.current;
2705
+ if (!box) return;
2706
+ const announce = (el) => {
2707
+ if (!el) return;
2708
+ const list = items();
2709
+ const index = list.indexOf(el);
2710
+ if (index >= 0) setCurrent({ id: el.id || null, index });
2711
+ };
2712
+ const nearest = () => {
2713
+ const list = items();
2714
+ const horizontal = box.scrollWidth > box.clientWidth;
2715
+ const origin = box.getBoundingClientRect();
2716
+ let best = null, distance = Infinity;
2717
+ for (const c of list) {
2718
+ const r = c.getBoundingClientRect();
2719
+ const d = Math.abs(horizontal ? r.left - origin.left : r.top - origin.top);
2720
+ if (d < distance) {
2721
+ distance = d;
2722
+ best = c;
2723
+ }
2724
+ }
2725
+ return best;
2726
+ };
2727
+ let timer;
2728
+ const native = "onscrollsnapchange" in box;
2729
+ const onSnap = (e) => {
2730
+ const ev = e;
2731
+ announce(ev.snapTargetInline || ev.snapTargetBlock);
2732
+ };
2733
+ const settle = () => {
2734
+ clearTimeout(timer);
2735
+ timer = setTimeout(() => announce(nearest()), 90);
2736
+ };
2737
+ if (native) box.addEventListener("scrollsnapchange", onSnap);
2738
+ else box.addEventListener("onscrollend" in box ? "scrollend" : "scroll", settle, { passive: true });
2739
+ announce(nearest());
2740
+ return () => {
2741
+ box.removeEventListener("scrollsnapchange", onSnap);
2742
+ box.removeEventListener("scrollend", settle);
2743
+ box.removeEventListener("scroll", settle);
2744
+ clearTimeout(timer);
2745
+ };
2746
+ }, [ref.current]);
2747
+ const to = (0, import_react22.useCallback)(
2748
+ (target) => {
2749
+ const list = items();
2750
+ const el = typeof target === "number" ? list[target] : list.find((c) => c.id === target);
2751
+ el?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" });
2752
+ },
2753
+ [items]
2754
+ );
2755
+ return { ...current, to };
2756
+ }
2757
+ var HAPTICS = {
2758
+ selection: [8],
2759
+ impact: [16],
2760
+ success: [10, 30, 20],
2761
+ warning: [25, 40, 25],
2762
+ error: [40, 30, 40, 30, 60]
2763
+ };
2764
+ function haptic(kind = "selection") {
2765
+ if (typeof navigator === "undefined") return false;
2766
+ if (typeof navigator.vibrate === "function") {
2767
+ navigator.vibrate(HAPTICS[kind]);
2768
+ return true;
2769
+ }
2770
+ if (/iphone|ipad|ipod/i.test(navigator.userAgent)) {
2771
+ let sw = document.getElementById("b-haptic-switch");
2772
+ if (!sw) {
2773
+ sw = document.createElement("input");
2774
+ sw.type = "checkbox";
2775
+ sw.id = "b-haptic-switch";
2776
+ sw.tabIndex = -1;
2777
+ sw.setAttribute("switch", "");
2778
+ sw.setAttribute("aria-hidden", "true");
2779
+ sw.style.cssText = "position:fixed;inset:auto auto 0 0;width:1px;height:1px;opacity:0;pointer-events:none";
2780
+ document.body.appendChild(sw);
2781
+ }
2782
+ sw.click();
2783
+ return true;
2784
+ }
2785
+ return false;
2786
+ }
2787
+ var PERSIST = "barua-persist:";
2788
+ function usePersistedState(key, initial) {
2789
+ const [value, setValue] = (0, import_react22.useState)(() => {
2790
+ if (typeof window === "undefined") return initial;
2791
+ try {
2792
+ const raw = localStorage.getItem(PERSIST + key);
2793
+ return raw === null ? initial : JSON.parse(raw);
2794
+ } catch {
2795
+ return initial;
2796
+ }
2797
+ });
2798
+ const set = (0, import_react22.useCallback)(
2799
+ (next) => {
2800
+ setValue((prev) => {
2801
+ const resolved = typeof next === "function" ? next(prev) : next;
2802
+ try {
2803
+ localStorage.setItem(PERSIST + key, JSON.stringify(resolved));
2804
+ } catch {
2805
+ }
2806
+ return resolved;
2807
+ });
2808
+ },
2809
+ [key]
2810
+ );
2811
+ return [value, set];
2812
+ }
2620
2813
  // Annotate the CommonJS export names for ESM import in node:
2621
2814
  0 && (module.exports = {
2622
2815
  Accordion,
@@ -2912,8 +3105,13 @@ var ContextMenuTarget = (0, import_react21.forwardRef)(
2912
3105
  block,
2913
3106
  buttonClasses,
2914
3107
  cn,
3108
+ haptic,
2915
3109
  iconNames,
2916
3110
  useBaruaTheme,
2917
- useToast
3111
+ usePersistedState,
3112
+ useScrollGeometry,
3113
+ useSnapPosition,
3114
+ useToast,
3115
+ useVisible
2918
3116
  });
2919
3117
  //# sourceMappingURL=index.cjs.map