@djangocfg/layouts 2.1.266 → 2.1.267

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.
@@ -1,2 +1,6 @@
1
1
  export { useMobileNavPanel } from './useMobileNavPanel';
2
-
2
+ export { useDropdownMenu } from './useDropdownMenu';
3
+ export type { UseDropdownMenuReturn } from './useDropdownMenu';
4
+ export { useNavbarScroll } from './useNavbarScroll';
5
+ export type { UseNavbarScrollOptions, UseNavbarScrollReturn } from './useNavbarScroll';
6
+ export { useNavbarViewportVars } from './useNavbarViewportVars';
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+
5
+ export interface UseDropdownMenuReturn {
6
+ openDropdownKey: string | null;
7
+ scheduleOpen: (key: string) => void;
8
+ scheduleClose: (key: string) => void;
9
+ closeDropdown: () => void;
10
+ }
11
+
12
+ /** Hover-based open/close with debounce timers for desktop dropdown menus. */
13
+ export function useDropdownMenu(): UseDropdownMenuReturn {
14
+ const [openDropdownKey, setOpenDropdownKey] = useState<string | null>(null);
15
+ const openTimerRef = useRef<number | null>(null);
16
+ const closeTimerRef = useRef<number | null>(null);
17
+
18
+ const clearOpenTimer = useCallback(() => {
19
+ if (openTimerRef.current !== null) {
20
+ window.clearTimeout(openTimerRef.current);
21
+ openTimerRef.current = null;
22
+ }
23
+ }, []);
24
+
25
+ const clearCloseTimer = useCallback(() => {
26
+ if (closeTimerRef.current !== null) {
27
+ window.clearTimeout(closeTimerRef.current);
28
+ closeTimerRef.current = null;
29
+ }
30
+ }, []);
31
+
32
+ const scheduleOpen = useCallback((key: string) => {
33
+ clearOpenTimer();
34
+ clearCloseTimer();
35
+ openTimerRef.current = window.setTimeout(() => {
36
+ setOpenDropdownKey(key);
37
+ }, 80);
38
+ }, [clearOpenTimer, clearCloseTimer]);
39
+
40
+ const scheduleClose = useCallback((key: string) => {
41
+ clearOpenTimer();
42
+ clearCloseTimer();
43
+ closeTimerRef.current = window.setTimeout(() => {
44
+ setOpenDropdownKey((prev) => (prev === key ? null : prev));
45
+ }, 120);
46
+ }, [clearOpenTimer, clearCloseTimer]);
47
+
48
+ const closeDropdown = useCallback(() => setOpenDropdownKey(null), []);
49
+
50
+ useEffect(() => {
51
+ return () => {
52
+ clearOpenTimer();
53
+ clearCloseTimer();
54
+ };
55
+ }, [clearOpenTimer, clearCloseTimer]);
56
+
57
+ return { openDropdownKey, scheduleOpen, scheduleClose, closeDropdown };
58
+ }
@@ -0,0 +1,61 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+
5
+ export interface UseNavbarScrollOptions {
6
+ /** Slide navbar up on scroll-down, back on scroll-up. @default false */
7
+ hideNavOnScroll?: boolean;
8
+ /** Transparent at page top, opaque after threshold. @default false */
9
+ transparent?: boolean;
10
+ /** scrollY threshold for hide + transparent triggers. @default 40 */
11
+ transparentThreshold?: number;
12
+ }
13
+
14
+ export interface UseNavbarScrollReturn {
15
+ /** true → apply `-translate-y-full` to outer wrapper */
16
+ hidden: boolean;
17
+ /** true → scrollY >= threshold; drives opaque background */
18
+ scrolled: boolean;
19
+ }
20
+
21
+ export function useNavbarScroll(options: UseNavbarScrollOptions = {}): UseNavbarScrollReturn {
22
+ const { hideNavOnScroll = false, transparent = false, transparentThreshold = 40 } = options;
23
+
24
+ const [hidden, setHidden] = useState(false);
25
+ const [scrolled, setScrolled] = useState(false);
26
+ const lastScrollY = useRef(0);
27
+
28
+ useEffect(() => {
29
+ if (!hideNavOnScroll && !transparent) {
30
+ setHidden(false);
31
+ setScrolled(false);
32
+ return;
33
+ }
34
+
35
+ const handleScroll = () => {
36
+ const currentY = window.scrollY;
37
+ const delta = currentY - lastScrollY.current;
38
+
39
+ if (transparent) {
40
+ setScrolled(currentY >= transparentThreshold);
41
+ }
42
+
43
+ if (hideNavOnScroll) {
44
+ if (currentY > transparentThreshold) {
45
+ if (delta > 0) setHidden(true);
46
+ else if (delta < 0) setHidden(false);
47
+ } else {
48
+ setHidden(false);
49
+ }
50
+ }
51
+
52
+ lastScrollY.current = currentY;
53
+ };
54
+
55
+ handleScroll();
56
+ window.addEventListener('scroll', handleScroll, { passive: true });
57
+ return () => window.removeEventListener('scroll', handleScroll);
58
+ }, [hideNavOnScroll, transparent, transparentThreshold]);
59
+
60
+ return { hidden, scrolled };
61
+ }
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import { type RefObject, useEffect } from 'react';
4
+
5
+ /**
6
+ * Tracks the bottom edge of the navbar outer element and writes CSS vars:
7
+ * --public-navbar-mobile-drawer-top
8
+ * --public-navbar-mobile-drawer-max-height
9
+ *
10
+ * @param navOuterRef - ref on the outermost navbar wrapper div
11
+ * @param deps - values that should re-trigger observer setup (position, variant, containerClassName)
12
+ */
13
+ export function useNavbarViewportVars(
14
+ navOuterRef: RefObject<HTMLDivElement | null>,
15
+ deps: readonly unknown[],
16
+ ): void {
17
+ useEffect(() => {
18
+ const update = () => {
19
+ const root = document.documentElement;
20
+ const navEl = navOuterRef.current;
21
+ if (!navEl) return;
22
+
23
+ const rect = navEl.getBoundingClientRect();
24
+ const top = Math.max(0, Math.round(rect.bottom + 8));
25
+ const maxHeight = Math.max(240, window.innerHeight - top - 12);
26
+
27
+ root.style.setProperty('--public-navbar-mobile-drawer-top', `${top}px`);
28
+ root.style.setProperty('--public-navbar-mobile-drawer-max-height', `${maxHeight}px`);
29
+ };
30
+
31
+ update();
32
+ const navEl = navOuterRef.current;
33
+ const observer = navEl ? new ResizeObserver(update) : null;
34
+ if (navEl && observer) observer.observe(navEl);
35
+ window.addEventListener('resize', update);
36
+ window.addEventListener('scroll', update, { passive: true });
37
+
38
+ return () => {
39
+ if (navEl && observer) observer.unobserve(navEl);
40
+ observer?.disconnect();
41
+ window.removeEventListener('resize', update);
42
+ window.removeEventListener('scroll', update);
43
+ };
44
+ // eslint-disable-next-line react-hooks/exhaustive-deps
45
+ }, deps);
46
+ }
@@ -10,7 +10,11 @@ export type {
10
10
  PublicNavbarVariant,
11
11
  PublicNavbarPosition,
12
12
  PublicNavbarShellConfig,
13
+ PublicNavLayout,
14
+ PublicNavbarHeight,
13
15
  } from './navbarTypes';
16
+ export type { UseNavbarScrollOptions, UseNavbarScrollReturn } from './hooks/useNavbarScroll';
17
+ export type { UseDropdownMenuReturn } from './hooks/useDropdownMenu';
14
18
  export type {
15
19
  PublicDesktopDropdownRenderer,
16
20
  PublicDesktopDropdownRenderProps,
@@ -6,6 +6,23 @@ export type PublicNavbarVariant = 'floating' | 'flush';
6
6
 
7
7
  export type PublicNavbarPosition = 'sticky' | 'fixed' | 'static';
8
8
 
9
+ /**
10
+ * Desktop nav layout variant.
11
+ * - `default` — brand left | nav **centered** (absolute) | actions right
12
+ * - `brand-left` — brand left | nav immediately after brand | actions pushed right
13
+ * - `centered` — brand + nav + actions all centered in one row
14
+ * - `split` — brand left | actions right | no desktop nav (drawer only)
15
+ */
16
+ export type PublicNavLayout = 'default' | 'brand-left' | 'centered' | 'split';
17
+
18
+ /**
19
+ * Navbar vertical padding / height.
20
+ * - `sm` → py-2
21
+ * - `md` → py-3.5 (default, matches current)
22
+ * - `lg` → py-5
23
+ */
24
+ export type PublicNavbarHeight = 'sm' | 'md' | 'lg';
25
+
9
26
  export interface PublicNavbarSurface {
10
27
  variant: PublicNavbarVariant;
11
28
  position: PublicNavbarPosition;
@@ -105,12 +105,15 @@ export function matchesPath(pathname: string, enabledPath?: string | string[]):
105
105
  if (!enabledPath) return false;
106
106
 
107
107
  const matchSinglePath = (path: string): boolean => {
108
+ // Normalize both sides — strip trailing slash (except root)
109
+ const normalPath = pathname.length > 1 ? pathname.replace(/\/+$/, '') : pathname;
110
+ const normalPattern = path.length > 1 ? path.replace(/\/+$/, '') : path;
108
111
  // If pattern contains glob characters, use pattern matching
109
- if (path.includes('*')) {
110
- return matchGlobPattern(pathname, path);
112
+ if (normalPattern.includes('*')) {
113
+ return matchGlobPattern(normalPath, normalPattern);
111
114
  }
112
115
  // Otherwise, exact or prefix match
113
- return pathname === path || pathname.startsWith(path + '/');
116
+ return normalPath === normalPattern || normalPath.startsWith(normalPattern + '/');
114
117
  };
115
118
 
116
119
  if (typeof enabledPath === 'string') {