@djangocfg/ui-core 2.1.280 → 2.1.282

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/README.md CHANGED
@@ -76,6 +76,7 @@ Default **`TooltipContent`** styling uses semantic **popover** tokens (`bg-popov
76
76
  | `useIsPhone()` | `< 640px` — phones only |
77
77
  | `useIsMobile()` | `< 768px` — phones + small tablets |
78
78
  | `useIsTabletOrBelow()` | `< 1024px` — phones + tablets |
79
+ | `useBodyScrollLock(locked)` | Lock body scroll while `locked=true`; counter-based (multi-consumer safe), iOS-safe via `position: fixed` fallback |
79
80
  | `useCopy` | Copy to clipboard |
80
81
  | `useCountdown` | Countdown timer |
81
82
  | `useDebounce` | Debounce values |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.280",
3
+ "version": "2.1.282",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -86,7 +86,7 @@
86
86
  "playground": "playground dev"
87
87
  },
88
88
  "peerDependencies": {
89
- "@djangocfg/i18n": "^2.1.280",
89
+ "@djangocfg/i18n": "^2.1.282",
90
90
  "consola": "^3.4.2",
91
91
  "lucide-react": "^0.545.0",
92
92
  "moment": "^2.30.1",
@@ -148,9 +148,9 @@
148
148
  "vaul": "1.1.2"
149
149
  },
150
150
  "devDependencies": {
151
- "@djangocfg/i18n": "^2.1.280",
151
+ "@djangocfg/i18n": "^2.1.282",
152
152
  "@djangocfg/playground": "workspace:*",
153
- "@djangocfg/typescript-config": "^2.1.280",
153
+ "@djangocfg/typescript-config": "^2.1.282",
154
154
  "@types/node": "^24.7.2",
155
155
  "@types/react": "^19.1.0",
156
156
  "@types/react-dom": "^19.1.0",
@@ -12,6 +12,7 @@ export { useDebounce } from './useDebounce';
12
12
  export { useDebugTools } from './useDebugTools';
13
13
  export { useEventListener, events } from './useEventsBus';
14
14
  export { useIsMobile, useIsPhone, useIsTabletOrBelow, BREAKPOINTS } from './useMobile';
15
+ export { useBodyScrollLock } from './useBodyScrollLock';
15
16
  export type { Breakpoint } from './useMobile';
16
17
  export { useMediaQuery, BREAKPOINTS as MEDIA_BREAKPOINTS } from './useMediaQuery';
17
18
  export { useCopy } from './useCopy';
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+
5
+ import { useDeviceDetect } from './useDeviceDetect';
6
+
7
+ /**
8
+ * useBodyScrollLock — locks body scroll while `locked` is true.
9
+ *
10
+ * Safe to use from multiple components concurrently: a module-scope counter
11
+ * tracks active locks and only releases the lock when the last caller unmounts
12
+ * (or passes `locked=false`).
13
+ *
14
+ * Strategy:
15
+ * - iOS Safari: pin body in place with `position: fixed; top: -scrollY` and
16
+ * restore scroll on release. `overflow: hidden` alone is ignored on iOS.
17
+ * - Other browsers: `body.overflow = 'hidden'` plus a `paddingRight`
18
+ * compensator for the disappearing scrollbar so layout doesn't shift.
19
+ * We intentionally do NOT touch `html.overflow` — that removes the
20
+ * scrolling container for `position: sticky` descendants and breaks
21
+ * sticky navbars while the lock is active.
22
+ *
23
+ * @example
24
+ * useBodyScrollLock(isMobile && drawerOpen);
25
+ */
26
+
27
+ let lockCount = 0;
28
+ interface SavedState {
29
+ bodyOverflow: string;
30
+ bodyPaddingRight: string;
31
+ bodyPosition: string;
32
+ bodyTop: string;
33
+ bodyWidth: string;
34
+ scrollY: number;
35
+ iosMode: boolean;
36
+ }
37
+ let saved: SavedState | null = null;
38
+
39
+ function getScrollbarWidth(): number {
40
+ if (typeof window === 'undefined') return 0;
41
+ return window.innerWidth - document.documentElement.clientWidth;
42
+ }
43
+
44
+ function applyLock(iosMode: boolean) {
45
+ const body = document.body;
46
+ const scrollY = window.scrollY;
47
+
48
+ saved = {
49
+ bodyOverflow: body.style.overflow,
50
+ bodyPaddingRight: body.style.paddingRight,
51
+ bodyPosition: body.style.position,
52
+ bodyTop: body.style.top,
53
+ bodyWidth: body.style.width,
54
+ scrollY,
55
+ iosMode,
56
+ };
57
+
58
+ if (iosMode) {
59
+ // iOS Safari ignores `overflow: hidden` on body — pin body in place and
60
+ // remember scroll position so we can restore it on release.
61
+ body.style.position = 'fixed';
62
+ body.style.top = `-${scrollY}px`;
63
+ body.style.width = '100%';
64
+ body.style.overflow = 'hidden';
65
+ } else {
66
+ // Avoid touching `html.overflow`: it breaks `position: sticky` ancestors
67
+ // (they lose their scrolling container and fall back to static flow).
68
+ const scrollbarWidth = getScrollbarWidth();
69
+ if (scrollbarWidth > 0) {
70
+ const current = parseFloat(window.getComputedStyle(body).paddingRight) || 0;
71
+ body.style.paddingRight = `${current + scrollbarWidth}px`;
72
+ }
73
+ body.style.overflow = 'hidden';
74
+ }
75
+ }
76
+
77
+ function releaseLock() {
78
+ if (!saved) return;
79
+ const body = document.body;
80
+ const { iosMode, scrollY } = saved;
81
+
82
+ body.style.overflow = saved.bodyOverflow;
83
+ body.style.paddingRight = saved.bodyPaddingRight;
84
+ body.style.position = saved.bodyPosition;
85
+ body.style.top = saved.bodyTop;
86
+ body.style.width = saved.bodyWidth;
87
+
88
+ if (iosMode) {
89
+ window.scrollTo(0, scrollY);
90
+ }
91
+
92
+ saved = null;
93
+ }
94
+
95
+ export function useBodyScrollLock(locked: boolean): void {
96
+ const device = useDeviceDetect();
97
+
98
+ useEffect(() => {
99
+ if (!locked) return;
100
+ if (typeof document === 'undefined') return;
101
+
102
+ if (lockCount === 0) {
103
+ applyLock(device.isIOS);
104
+ }
105
+ lockCount += 1;
106
+
107
+ return () => {
108
+ lockCount = Math.max(0, lockCount - 1);
109
+ if (lockCount === 0) {
110
+ releaseLock();
111
+ }
112
+ };
113
+ }, [locked, device.isIOS]);
114
+ }
@@ -4,6 +4,27 @@
4
4
  * Compatible with Tailwind CSS v4
5
5
  */
6
6
 
7
+ /**
8
+ * Display font utility — for marketing headlines and large numeric stats.
9
+ *
10
+ * The font itself is NOT shipped here. Apps pick their own display font and
11
+ * expose it as `--font-display` on <html> (via `next/font`, `@font-face`, or
12
+ * equivalent). If the variable is missing, falls back to the system UI stack.
13
+ *
14
+ * @example (next/font in an app layout)
15
+ * const display = Plus_Jakarta_Sans({ weight: ['700', '800'], variable: '--font-display' });
16
+ * <html className={display.variable}>
17
+ *
18
+ * @example (usage)
19
+ * <h1 className="font-display text-7xl font-black tracking-tight">Ship fast.</h1>
20
+ */
21
+ .font-display {
22
+ font-family: var(--font-display), system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
23
+ font-feature-settings: 'ss01', 'ss03';
24
+ letter-spacing: -0.035em;
25
+ padding-bottom: 0.08em;
26
+ }
27
+
7
28
  /**
8
29
  * Screen Reader Only
9
30
  * Hides content visually but keeps it accessible to screen readers