@djangocfg/ui-core 2.1.293 → 2.1.297

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.
Files changed (73) hide show
  1. package/README.md +127 -26
  2. package/package.json +16 -4
  3. package/src/components/feedback/sonner/index.tsx +1 -1
  4. package/src/components/forms/button/index.tsx +21 -5
  5. package/src/components/forms/button-download/index.tsx +1 -1
  6. package/src/components/forms/input/index.tsx +1 -1
  7. package/src/components/forms/otp/index.tsx +1 -1
  8. package/src/components/forms/slider/index.tsx +1 -1
  9. package/src/components/forms/textarea/index.tsx +1 -1
  10. package/src/components/index.ts +2 -0
  11. package/src/components/layout/sticky/index.tsx +1 -1
  12. package/src/components/navigation/accordion/index.tsx +1 -1
  13. package/src/components/navigation/dropdown-menu/index.tsx +3 -2
  14. package/src/components/navigation/link/Link.tsx +124 -0
  15. package/src/components/navigation/link/LinkContext.tsx +52 -0
  16. package/src/components/navigation/link/index.ts +8 -0
  17. package/src/components/navigation/menubar/index.tsx +3 -2
  18. package/src/components/navigation/navigation-menu/index.tsx +2 -1
  19. package/src/components/navigation/tabs/index.tsx +1 -1
  20. package/src/components/overlay/responsive-sheet/index.tsx +1 -1
  21. package/src/components/select/combobox.tsx +1 -1
  22. package/src/components/select/multi-select.tsx +1 -1
  23. package/src/components/specialized/image-with-fallback/index.tsx +1 -1
  24. package/src/hooks/debug/index.ts +3 -0
  25. package/src/hooks/device/index.ts +7 -0
  26. package/src/hooks/dom/index.ts +12 -0
  27. package/src/hooks/{useBodyScrollLock.ts → dom/useBodyScrollLock.ts} +1 -1
  28. package/src/hooks/{useCopy.ts → dom/useCopy.ts} +1 -1
  29. package/src/hooks/dom/useScroll.ts +322 -0
  30. package/src/hooks/events/index.ts +3 -0
  31. package/src/hooks/feedback/index.ts +3 -0
  32. package/src/hooks/hotkey/index.ts +4 -0
  33. package/src/hooks/index.ts +82 -26
  34. package/src/hooks/media/index.ts +5 -0
  35. package/src/hooks/router/README.md +121 -0
  36. package/src/hooks/router/adapter.tsx +139 -0
  37. package/src/hooks/router/adapters/index.ts +5 -0
  38. package/src/hooks/router/adapters/nextjs.tsx +140 -0
  39. package/src/hooks/router/index.ts +90 -0
  40. package/src/hooks/router/parsers.ts +154 -0
  41. package/src/hooks/router/useBackOrFallback.ts +145 -0
  42. package/src/hooks/router/useIsActive.ts +60 -0
  43. package/src/hooks/router/useLocation.ts +163 -0
  44. package/src/hooks/router/useNavigate.ts +96 -0
  45. package/src/hooks/router/useQueryParams.ts +262 -0
  46. package/src/hooks/router/useQueryState.ts +106 -0
  47. package/src/hooks/router/useRouter.ts +81 -0
  48. package/src/hooks/router/useSmartLink.ts +157 -0
  49. package/src/hooks/router/useUrlBuilder.ts +118 -0
  50. package/src/hooks/state/index.ts +8 -0
  51. package/src/hooks/theme/index.ts +4 -0
  52. package/src/hooks/time/index.ts +4 -0
  53. package/src/lib/dialog-service/dialogs/AlertDialogUI.tsx +1 -1
  54. package/src/lib/dialog-service/dialogs/ConfirmDialogUI.tsx +1 -1
  55. package/src/lib/dialog-service/dialogs/PromptDialogUI.tsx +1 -1
  56. package/src/styles/palette/useThemePalette.ts +1 -1
  57. /package/src/hooks/{useDebugTools.ts → debug/useDebugTools.ts} +0 -0
  58. /package/src/hooks/{useBrowserDetect.ts → device/useBrowserDetect.ts} +0 -0
  59. /package/src/hooks/{useDeviceDetect.ts → device/useDeviceDetect.ts} +0 -0
  60. /package/src/hooks/{useShortcutModLabel.ts → device/useShortcutModLabel.ts} +0 -0
  61. /package/src/hooks/{useImageLoader.ts → dom/useImageLoader.ts} +0 -0
  62. /package/src/hooks/{useEventsBus.ts → events/useEventsBus.ts} +0 -0
  63. /package/src/hooks/{useToast.ts → feedback/useToast.ts} +0 -0
  64. /package/src/hooks/{useHotkey.ts → hotkey/useHotkey.ts} +0 -0
  65. /package/src/hooks/{useMediaQuery.ts → media/useMediaQuery.ts} +0 -0
  66. /package/src/hooks/{useMobile.tsx → media/useMobile.tsx} +0 -0
  67. /package/src/hooks/{useDebounce.ts → state/useDebounce.ts} +0 -0
  68. /package/src/hooks/{useDebouncedCallback.ts → state/useDebouncedCallback.ts} +0 -0
  69. /package/src/hooks/{useLocalStorage.ts → state/useLocalStorage.ts} +0 -0
  70. /package/src/hooks/{useSessionStorage.ts → state/useSessionStorage.ts} +0 -0
  71. /package/src/hooks/{useStoredValue.ts → state/useStoredValue.ts} +0 -0
  72. /package/src/hooks/{useResolvedTheme.ts → theme/useResolvedTheme.ts} +0 -0
  73. /package/src/hooks/{useCountdown.ts → time/useCountdown.ts} +0 -0
@@ -0,0 +1,81 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * useRouter — composite facade over the atomic router hooks.
5
+ *
6
+ * WHY:
7
+ * For consumers who want a single import that "feels like" Next's
8
+ * `useRouter`. Composes useLocation + useNavigate + useQueryParams +
9
+ * useBackOrFallback + useUrlBuilder.
10
+ *
11
+ * NOTE on perf and tree-shaking:
12
+ * This hook subscribes to EVERY URL change (location, search, depth)
13
+ * because it composes hooks that subscribe to those things. If your
14
+ * component only needs `navigate` (no location read), prefer the
15
+ * atomic `useNavigate()` to avoid extra re-renders. Same for
16
+ * `useQueryParams`, `useUrlBuilder`, etc. The atomic hooks also
17
+ * tree-shake better — pulling in just `useNavigate` doesn't pay the
18
+ * cost of the snapshot machinery.
19
+ *
20
+ * @example
21
+ * const router = useRouter();
22
+ * router.navigate('/users');
23
+ * router.query.set({ page: 2 });
24
+ * router.backOrFallback('/dashboard');
25
+ */
26
+
27
+ import { useMemo } from 'react';
28
+ import { useLocation, type LocationSnapshot } from './useLocation';
29
+ import { useNavigate, type UseNavigateReturn } from './useNavigate';
30
+ import { useQueryParams, type UseQueryParamsReturn } from './useQueryParams';
31
+ import { useBackOrFallback } from './useBackOrFallback';
32
+ import { useUrlBuilder, type UseUrlBuilderReturn } from './useUrlBuilder';
33
+
34
+ export interface UseRouterReturn extends LocationSnapshot, UseNavigateReturn {
35
+ /** Same as `useQueryParams()`. */
36
+ query: UseQueryParamsReturn;
37
+ /** Same as `useUrlBuilder().build`. */
38
+ build: UseUrlBuilderReturn['build'];
39
+ /** Same as `useUrlBuilder().withCurrentParams`. */
40
+ withCurrentParams: UseUrlBuilderReturn['withCurrentParams'];
41
+ /** Same as `useBackOrFallback().back`. */
42
+ backOrFallback: (fallback?: string) => void;
43
+ /** Same as `useBackOrFallback().canGoBack`. */
44
+ canGoBack: boolean;
45
+ }
46
+
47
+ /**
48
+ * Convenience hook that exposes the full router surface.
49
+ * For minimal re-renders / smaller bundles, prefer the atomic hooks.
50
+ */
51
+ export function useRouter(): UseRouterReturn {
52
+ const location = useLocation();
53
+ const nav = useNavigate();
54
+ const query = useQueryParams();
55
+ const builder = useUrlBuilder();
56
+ const { back: backOrFallback, canGoBack } = useBackOrFallback();
57
+
58
+ return useMemo<UseRouterReturn>(
59
+ () => ({
60
+ // Location snapshot
61
+ pathname: location.pathname,
62
+ search: location.search,
63
+ hash: location.hash,
64
+ href: location.href,
65
+ // Navigation pass-through
66
+ navigate: nav.navigate,
67
+ navigateExternal: nav.navigateExternal,
68
+ push: nav.push,
69
+ replace: nav.replace,
70
+ back: nav.back,
71
+ forward: nav.forward,
72
+ // Sub-APIs
73
+ query,
74
+ build: builder.build,
75
+ withCurrentParams: builder.withCurrentParams,
76
+ backOrFallback,
77
+ canGoBack,
78
+ }),
79
+ [location, nav, query, builder, backOrFallback, canGoBack]
80
+ );
81
+ }
@@ -0,0 +1,157 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * useSmartLink — turn a non-`<a>` element (card, table row, list item)
5
+ * into a proper link.
6
+ *
7
+ * WHY:
8
+ * "Clickable cards" used to either nest an `<a>` (which then can't
9
+ * contain other interactive children) or attach `onClick={navigate}`
10
+ * (which loses cmd-click, middle-click, keyboard, accessibility).
11
+ * This hook returns a small bag of handlers that gives a non-anchor
12
+ * element the right behavior in all those cases.
13
+ *
14
+ * Key behaviors:
15
+ * - Cmd/Ctrl+click and middle-click open in a new tab.
16
+ * - Plain click does SPA nav.
17
+ * - Enter / Space activate from keyboard.
18
+ * - Clicks inside nested `<a>` / `<button>` are ignored (so the
19
+ * inner element handles its own action).
20
+ * - `role="link"` and `tabIndex={0}` for screen readers / keyboard.
21
+ *
22
+ * @example
23
+ * const link = useSmartLink('/products/42');
24
+ * <div {...link}>Product card</div>
25
+ */
26
+
27
+ import { useCallback, useMemo, type KeyboardEvent, type MouseEvent } from 'react';
28
+ import { useNavigate, type NavigateOptions } from './useNavigate';
29
+
30
+ export interface UseSmartLinkOptions extends NavigateOptions {
31
+ /** Disable all navigation (e.g. while a row is being edited). */
32
+ disabled?: boolean;
33
+ /**
34
+ * If true, modifier-clicks (cmd/ctrl) and middle-clicks DON'T open
35
+ * a new tab — they're treated as plain clicks. Default: false.
36
+ */
37
+ ignoreModifiers?: boolean;
38
+ }
39
+
40
+ export interface SmartLinkHandlers {
41
+ onClick: (event: MouseEvent<HTMLElement>) => void;
42
+ onAuxClick: (event: MouseEvent<HTMLElement>) => void;
43
+ onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
44
+ role: 'link';
45
+ /** -1 when disabled so the element is removed from the tab order. */
46
+ tabIndex: 0 | -1;
47
+ /** Forwarded to the consumer element so AT announces disabled state. */
48
+ 'aria-disabled'?: true;
49
+ }
50
+
51
+ const INTERACTIVE_SELECTOR =
52
+ 'a,button,input,textarea,select,label,[role="button"],[role="link"],[role="checkbox"],[role="switch"],[role="tab"],[role="menuitem"]';
53
+
54
+ /**
55
+ * True if the click target is inside another interactive element
56
+ * nested within `currentTarget` — in which case we shouldn't intercept.
57
+ * Uses Element.closest for a single C-side traversal that also handles
58
+ * SVG/MathML, custom elements, and `:where()` semantics correctly.
59
+ */
60
+ function isInsideNestedInteractive(event: MouseEvent<HTMLElement>): boolean {
61
+ const target = event.target as Element | null;
62
+ const current = event.currentTarget;
63
+ if (!target || target === current) return false;
64
+ const interactive = target.closest(INTERACTIVE_SELECTOR);
65
+ // Found an interactive ancestor strictly between target and currentTarget.
66
+ return !!interactive && interactive !== current && current.contains(interactive);
67
+ }
68
+
69
+ /** True if the user has selected text — don't navigate, they're reading. */
70
+ function hasTextSelection(): boolean {
71
+ if (typeof window === 'undefined') return false;
72
+ const selection = window.getSelection();
73
+ return !!selection && selection.toString().length > 0;
74
+ }
75
+
76
+ /**
77
+ * Hook variant — gives an element link semantics with keyboard + new-tab support.
78
+ */
79
+ export function useSmartLink(
80
+ href: string,
81
+ options?: UseSmartLinkOptions
82
+ ): SmartLinkHandlers {
83
+ const { navigate } = useNavigate();
84
+ const disabled = options?.disabled ?? false;
85
+ const ignoreModifiers = options?.ignoreModifiers ?? false;
86
+ const replace = options?.replace;
87
+ const scroll = options?.scroll;
88
+
89
+ const openInNewTab = useCallback((url: string) => {
90
+ if (typeof window === 'undefined') return;
91
+ window.open(url, '_blank', 'noopener,noreferrer');
92
+ }, []);
93
+
94
+ const onClick = useCallback(
95
+ (event: MouseEvent<HTMLElement>) => {
96
+ if (disabled) return;
97
+ if (event.defaultPrevented) return;
98
+ if (isInsideNestedInteractive(event)) return;
99
+ if (hasTextSelection()) return;
100
+
101
+ // Modifier click → open in new tab. Don't preventDefault on a
102
+ // real <a>, but for div/span our default IS to navigate so we
103
+ // can branch freely.
104
+ if (
105
+ !ignoreModifiers &&
106
+ (event.metaKey || event.ctrlKey || event.shiftKey)
107
+ ) {
108
+ event.preventDefault();
109
+ openInNewTab(href);
110
+ return;
111
+ }
112
+
113
+ event.preventDefault();
114
+ navigate(href, { replace, scroll });
115
+ },
116
+ [disabled, ignoreModifiers, navigate, href, replace, scroll, openInNewTab]
117
+ );
118
+
119
+ const onAuxClick = useCallback(
120
+ (event: MouseEvent<HTMLElement>) => {
121
+ if (disabled) return;
122
+ if (event.defaultPrevented) return;
123
+ if (isInsideNestedInteractive(event)) return;
124
+ // Middle-click only.
125
+ if (event.button !== 1) return;
126
+ if (ignoreModifiers) return;
127
+ event.preventDefault();
128
+ openInNewTab(href);
129
+ },
130
+ [disabled, ignoreModifiers, href, openInNewTab]
131
+ );
132
+
133
+ const onKeyDown = useCallback(
134
+ (event: KeyboardEvent<HTMLElement>) => {
135
+ if (disabled) return;
136
+ // Only handle when the focused element IS the link container,
137
+ // otherwise we steal Enter from inputs etc.
138
+ if (event.target !== event.currentTarget) return;
139
+ if (event.key !== 'Enter' && event.key !== ' ') return;
140
+ event.preventDefault();
141
+ navigate(href, { replace, scroll });
142
+ },
143
+ [disabled, navigate, href, replace, scroll]
144
+ );
145
+
146
+ return useMemo<SmartLinkHandlers>(
147
+ () => ({
148
+ onClick,
149
+ onAuxClick,
150
+ onKeyDown,
151
+ role: 'link',
152
+ tabIndex: disabled ? -1 : 0,
153
+ ...(disabled ? { 'aria-disabled': true as const } : {}),
154
+ }),
155
+ [onClick, onAuxClick, onKeyDown, disabled]
156
+ );
157
+ }
@@ -0,0 +1,118 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * useUrlBuilder — pure URL/querystring assembly.
5
+ *
6
+ * WHY:
7
+ * Building URLs by hand (template literals + URLSearchParams) is fiddly:
8
+ * you have to remember to skip empty values, encode keys, handle arrays.
9
+ * This hook centralizes those rules so callers stay declarative.
10
+ * Zero side effects — only React-bound thing is `useCallback` for ref
11
+ * stability inside JSX.
12
+ *
13
+ * @example
14
+ * const { build, withCurrentParams } = useUrlBuilder();
15
+ * build('/products', { page: 2, tag: ['a', 'b'], q: '' });
16
+ * // '/products?page=2&tag=a&tag=b'
17
+ * withCurrentParams('/products', { page: 1 });
18
+ * // keeps everything in current ?…, overrides `page`
19
+ */
20
+
21
+ import { useCallback, useMemo } from 'react';
22
+ import { useLocation } from './useLocation';
23
+
24
+ /** Value types accepted by `build`. Empty/null/undefined are stripped. */
25
+ export type QueryValue = string | number | boolean | null | undefined;
26
+ /** Param map. Arrays become repeated keys (`?tag=a&tag=b`), not csv. */
27
+ export type QueryParamsInput = Record<string, QueryValue | QueryValue[]>;
28
+
29
+ function appendValue(
30
+ params: URLSearchParams,
31
+ key: string,
32
+ value: QueryValue
33
+ ): void {
34
+ if (value === null || value === undefined) return;
35
+ if (typeof value === 'string' && value === '') return;
36
+ // Booleans serialize as 'true'/'false' — common-sense default.
37
+ params.append(key, String(value));
38
+ }
39
+
40
+ /**
41
+ * Build a query-string fragment (no leading `?`) from a param map.
42
+ * Skips empty / null / undefined values; arrays become repeated keys.
43
+ * Exported standalone so utilities can share the same rules without a hook.
44
+ */
45
+ export function buildQueryString(params?: QueryParamsInput): string {
46
+ if (!params) return '';
47
+ const search = new URLSearchParams();
48
+ for (const key of Object.keys(params)) {
49
+ const value = params[key];
50
+ if (Array.isArray(value)) {
51
+ for (const item of value) appendValue(search, key, item);
52
+ } else {
53
+ appendValue(search, key, value);
54
+ }
55
+ }
56
+ return search.toString();
57
+ }
58
+
59
+ /**
60
+ * Build a full path: `path + '?' + qs` (or just `path` if qs is empty).
61
+ * Pure — useful outside React (link generators, server code).
62
+ */
63
+ export function buildUrl(path: string, params?: QueryParamsInput): string {
64
+ const qs = buildQueryString(params);
65
+ return qs ? `${path}?${qs}` : path;
66
+ }
67
+
68
+ export interface UseUrlBuilderReturn {
69
+ /** Assemble `path` + serialized params. */
70
+ build: (path: string, params?: QueryParamsInput) => string;
71
+ /**
72
+ * Assemble `path` keeping the current page's querystring,
73
+ * with `overrides` merged on top. Pass `null`/`undefined`/`''` in
74
+ * `overrides` to drop a key.
75
+ */
76
+ withCurrentParams: (
77
+ path: string,
78
+ overrides?: QueryParamsInput
79
+ ) => string;
80
+ }
81
+
82
+ /**
83
+ * Stable URL builder helpers. `withCurrentParams` re-renders when the
84
+ * current querystring changes (it reads `useLocation` internally).
85
+ */
86
+ export function useUrlBuilder(): UseUrlBuilderReturn {
87
+ const { search } = useLocation();
88
+
89
+ const build = useCallback((path: string, params?: QueryParamsInput): string => {
90
+ return buildUrl(path, params);
91
+ }, []);
92
+
93
+ const withCurrentParams = useCallback(
94
+ (path: string, overrides?: QueryParamsInput): string => {
95
+ const next = new URLSearchParams(search);
96
+ if (overrides) {
97
+ for (const key of Object.keys(overrides)) {
98
+ const value = overrides[key];
99
+ // Overrides semantics: drop on empty, replace otherwise.
100
+ next.delete(key);
101
+ if (Array.isArray(value)) {
102
+ for (const item of value) appendValue(next, key, item);
103
+ } else {
104
+ appendValue(next, key, value);
105
+ }
106
+ }
107
+ }
108
+ const qs = next.toString();
109
+ return qs ? `${path}?${qs}` : path;
110
+ },
111
+ [search]
112
+ );
113
+
114
+ return useMemo<UseUrlBuilderReturn>(
115
+ () => ({ build, withCurrentParams }),
116
+ [build, withCurrentParams]
117
+ );
118
+ }
@@ -0,0 +1,8 @@
1
+ 'use client';
2
+
3
+ export { useDebounce } from './useDebounce';
4
+ export { useDebouncedCallback } from './useDebouncedCallback';
5
+ export { useLocalStorage } from './useLocalStorage';
6
+ export { useSessionStorage } from './useSessionStorage';
7
+ export { useStoredValue } from './useStoredValue';
8
+ export type { UseStoredValueOptions, StorageType } from './useStoredValue';
@@ -0,0 +1,4 @@
1
+ 'use client';
2
+
3
+ export { useResolvedTheme } from './useResolvedTheme';
4
+ export type { ResolvedTheme } from './useResolvedTheme';
@@ -0,0 +1,4 @@
1
+ 'use client';
2
+
3
+ export { useCountdown, useCountdownFromSeconds } from './useCountdown';
4
+ export type { CountdownState } from './useCountdown';
@@ -11,7 +11,7 @@ import {
11
11
  AlertDialogHeader,
12
12
  AlertDialogTitle,
13
13
  } from '../../../components/overlay/alert-dialog';
14
- import { useHotkey } from '../../../hooks/useHotkey';
14
+ import { useHotkey } from '../../../hooks';
15
15
  import { I18N_KEYS } from '../constants';
16
16
  import type { DialogOptions } from '../types';
17
17
 
@@ -13,7 +13,7 @@ import {
13
13
  AlertDialogTitle,
14
14
  } from '../../../components/overlay/alert-dialog';
15
15
  import { buttonVariants } from '../../../components/forms/button';
16
- import { useHotkey } from '../../../hooks/useHotkey';
16
+ import { useHotkey } from '../../../hooks';
17
17
  import { cn } from '../../utils';
18
18
  import { I18N_KEYS } from '../constants';
19
19
  import type { DialogOptions } from '../types';
@@ -12,7 +12,7 @@ import {
12
12
  } from '../../../components/overlay/dialog';
13
13
  import { Button } from '../../../components/forms/button';
14
14
  import { Input } from '../../../components/forms/input';
15
- import { useHotkey } from '../../../hooks/useHotkey';
15
+ import { useHotkey } from '../../../hooks';
16
16
  import { I18N_KEYS } from '../constants';
17
17
  import type { DialogOptions } from '../types';
18
18
 
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import { useMemo } from 'react';
18
- import { useResolvedTheme } from '../../hooks/useResolvedTheme';
18
+ import { useResolvedTheme } from '../../hooks';
19
19
  import { hslToHex } from './utils';
20
20
  import type { ThemePalette, StylePresets, BoxColors } from './types';
21
21
 
File without changes
File without changes