@djangocfg/ui-core 2.1.292 → 2.1.294

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
@@ -83,7 +83,7 @@ Default **`TooltipContent`** styling uses semantic **popover** tokens (`bg-popov
83
83
  ### Specialized (7)
84
84
  `Kbd` `TokenIcon` `Item` `Portal` `ImageWithFallback` `CopyButton` `CopyField`
85
85
 
86
- ## Hooks (17)
86
+ ## Hooks (28+)
87
87
 
88
88
  | Hook | Description |
89
89
  |------|-------------|
@@ -118,6 +118,53 @@ const isNarrow = useMediaQuery(`(max-width: ${BREAKPOINTS.sm - 1}px)`)
118
118
  const isDark = useMediaQuery('(prefers-color-scheme: dark)')
119
119
  ```
120
120
 
121
+ ## Router Hooks
122
+
123
+ Framework-agnostic navigation primitives — work on plain History API by default, plug into any router via the adapter pattern. Full docs in [`src/hooks/router/README.md`](./src/hooks/router/README.md).
124
+
125
+ | Hook | Purpose |
126
+ |------|---------|
127
+ | `useLocation()` | Reactive `{ pathname, search, hash, href }`. |
128
+ | `useLocationProperty(get, getSsr)` | Subscribe to ONE field — skip re-renders on unrelated changes. |
129
+ | `useNavigate()` | `navigate`, `navigateExternal`, `push`, `replace`, `back`, `forward`. |
130
+ | `useQueryParams()` | Record-style read/write of `?key=value` URL state. |
131
+ | `useQueryState(key, parser)` | Typed `useState`-style hook bound to one URL key (with `clearOnDefault`). |
132
+ | `useBackOrFallback()` | Smart Back that falls back to a route when there's no in-app history. |
133
+ | `useUrlBuilder()` | Pure URL assembly: `build`, `withCurrentParams`. |
134
+ | `useSmartLink(href)` | Make any element a link (cmd-click, middle-click, Enter, Space). |
135
+ | `useIsActive(href)` | Boolean for nav-item highlighting. |
136
+ | `useRouter()` | Convenience facade composing everything. |
137
+ | `RouterAdapterProvider` | Swap the navigation backend. |
138
+ | `parseAsString` / `parseAsInteger` / `parseAsFloat` / `parseAsBoolean` / `parseAsIsoDate` / `parseAsStringEnum` / `parseAsArrayOf` / `parseAsJson` | Parsers for `useQueryState`. Each has `.withDefault(value)`. |
139
+
140
+ ```tsx
141
+ import {
142
+ useNavigate,
143
+ useQueryState,
144
+ parseAsInteger,
145
+ } from '@djangocfg/ui-core/hooks';
146
+
147
+ const { navigate } = useNavigate();
148
+ const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
149
+
150
+ navigate('/products');
151
+ setPage((p) => p + 1); // ?page=2 — `page=1` is dropped (clearOnDefault)
152
+ ```
153
+
154
+ ### Next.js adapter
155
+
156
+ In Next apps, mount the adapter once near the root so navigation flows through `next/navigation` (server components + prefetch keep working):
157
+
158
+ ```tsx
159
+ import { NextRouterAdapter } from '@djangocfg/ui-core/adapters/nextjs';
160
+
161
+ <NextRouterAdapter>
162
+ <App />
163
+ </NextRouterAdapter>
164
+ ```
165
+
166
+ `next` is an **optional peer dependency** — Wails / Electron / Vite consumers don't pull it in.
167
+
121
168
  ## Theme Palette Hooks
122
169
 
123
170
  Hooks for accessing theme colors from CSS variables (useful for Canvas, SVG, charts, diagrams, etc.):
@@ -290,7 +337,8 @@ import '@djangocfg/ui-core/styles/globals';
290
337
  |------|---------|
291
338
  | `@djangocfg/ui-core` | All components & hooks |
292
339
  | `@djangocfg/ui-core/components` | Components only |
293
- | `@djangocfg/ui-core/hooks` | Hooks only |
340
+ | `@djangocfg/ui-core/hooks` | Hooks only (incl. router hooks) |
341
+ | `@djangocfg/ui-core/adapters/nextjs` | `<NextRouterAdapter>` for Next.js apps (optional peer: `next`) |
294
342
  | `@djangocfg/ui-core/lib` | Utilities (cn, etc.) |
295
343
  | `@djangocfg/ui-core/lib/dialog-service` | Dialog service |
296
344
  | `@djangocfg/ui-core/utils` | Runtime utilities (emitRuntimeError) |
@@ -322,7 +370,6 @@ These features require Next.js or browser storage APIs:
322
370
  - `DropdownMenu` — uses next/link
323
371
  - `DownloadButton` — uses localStorage
324
372
  - `useTheme` — uses next-themes
325
- - `useQueryParams`, `useCfgRouter` — uses next/router
326
373
 
327
374
  ## Requirements
328
375
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.292",
3
+ "version": "2.1.294",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -48,6 +48,11 @@
48
48
  "import": "./src/hooks/index.ts",
49
49
  "require": "./src/hooks/index.ts"
50
50
  },
51
+ "./adapters/nextjs": {
52
+ "types": "./src/hooks/router/adapters/nextjs.tsx",
53
+ "import": "./src/hooks/router/adapters/nextjs.tsx",
54
+ "require": "./src/hooks/router/adapters/nextjs.tsx"
55
+ },
51
56
  "./lib": {
52
57
  "types": "./src/lib/index.ts",
53
58
  "import": "./src/lib/index.ts",
@@ -86,10 +91,11 @@
86
91
  "playground": "playground dev"
87
92
  },
88
93
  "peerDependencies": {
89
- "@djangocfg/i18n": "^2.1.292",
94
+ "@djangocfg/i18n": "^2.1.294",
90
95
  "consola": "^3.4.2",
91
96
  "lucide-react": "^0.545.0",
92
97
  "moment": "^2.30.1",
98
+ "next": ">=14.0.0",
93
99
  "react": "^19.1.0",
94
100
  "react-device-detect": "^2.2.3",
95
101
  "react-dom": "^19.1.0",
@@ -98,6 +104,11 @@
98
104
  "zod": "^4.3.6",
99
105
  "zustand": "^5.0.0"
100
106
  },
107
+ "peerDependenciesMeta": {
108
+ "next": {
109
+ "optional": true
110
+ }
111
+ },
101
112
  "dependencies": {
102
113
  "@hookform/resolvers": "^5.2.2",
103
114
  "@radix-ui/react-accordion": "^1.2.12",
@@ -148,13 +159,14 @@
148
159
  "vaul": "1.1.2"
149
160
  },
150
161
  "devDependencies": {
151
- "@djangocfg/i18n": "^2.1.292",
162
+ "@djangocfg/i18n": "^2.1.294",
152
163
  "@djangocfg/playground": "workspace:*",
153
- "@djangocfg/typescript-config": "^2.1.292",
164
+ "@djangocfg/typescript-config": "^2.1.294",
154
165
  "@types/node": "^24.7.2",
155
166
  "@types/react": "^19.1.0",
156
167
  "@types/react-dom": "^19.1.0",
157
168
  "lucide-react": "^0.545.0",
169
+ "next": "^16.2.4",
158
170
  "typescript": "^5.9.3"
159
171
  },
160
172
  "publishConfig": {
@@ -31,3 +31,70 @@ export { useBrowserDetect } from './useBrowserDetect';
31
31
  export type { BrowserInfo } from './useBrowserDetect';
32
32
  export { useDeviceDetect } from './useDeviceDetect';
33
33
  export type { DeviceDetectResult } from './useDeviceDetect';
34
+
35
+ // ----------------------------------------------------------------------------
36
+ // Router — framework-agnostic navigation primitives.
37
+ // See ./router/README.md for design notes and the Next.js adapter example.
38
+ // ----------------------------------------------------------------------------
39
+ export {
40
+ // Adapter
41
+ RouterAdapterContext,
42
+ RouterAdapterProvider,
43
+ defaultAdapter,
44
+ useRouterAdapter,
45
+ // useLocation
46
+ useLocation,
47
+ NAVIGATE_EVENT,
48
+ // useNavigate
49
+ useNavigate,
50
+ // useQueryParams
51
+ useQueryParams,
52
+ // useBackOrFallback
53
+ useBackOrFallback,
54
+ // useUrlBuilder
55
+ useUrlBuilder,
56
+ buildUrl,
57
+ buildQueryString,
58
+ // useSmartLink
59
+ useSmartLink,
60
+ // useIsActive
61
+ useIsActive,
62
+ // useQueryState (typed single-key URL state)
63
+ useQueryState,
64
+ // Parsers
65
+ parseAsString,
66
+ parseAsInteger,
67
+ parseAsFloat,
68
+ parseAsBoolean,
69
+ parseAsIsoDate,
70
+ parseAsStringEnum,
71
+ parseAsArrayOf,
72
+ parseAsJson,
73
+ // useRouter (composite facade)
74
+ useRouter,
75
+ } from './router';
76
+ export type {
77
+ RouterAdapter,
78
+ RouterAdapterProviderProps,
79
+ RouterLocation,
80
+ LocationSnapshot,
81
+ NavigateOptions,
82
+ UseNavigateReturn,
83
+ QueryParamsSnapshot,
84
+ QueryParamValue,
85
+ QueryParamUpdates,
86
+ SetQueryParamsOptions,
87
+ UseQueryParamsReturn,
88
+ UseBackOrFallbackReturn,
89
+ QueryValue,
90
+ QueryParamsInput,
91
+ UseUrlBuilderReturn,
92
+ UseSmartLinkOptions,
93
+ SmartLinkHandlers,
94
+ UseIsActiveOptions,
95
+ UseQueryStateOptions,
96
+ QueryStateUpdater,
97
+ QueryParser,
98
+ QueryParserBuilder,
99
+ UseRouterReturn,
100
+ } from './router';
@@ -0,0 +1,119 @@
1
+ # Router hooks
2
+
3
+ Framework-agnostic navigation primitives for `@djangocfg/ui-core`. Built on plain browser APIs (`window.history`, `window.location`, `URLSearchParams`, `popstate`). No `next/*`, no `react-router`, no third-party deps.
4
+
5
+ ## Surface
6
+
7
+ | Hook | Purpose |
8
+ | --- | --- |
9
+ | `useLocation` | Reactive snapshot of `window.location` (`pathname`, `search`, `hash`, `href`). |
10
+ | `useNavigate` | Programmatic navigation: `navigate`, `navigateExternal`, `push`, `replace`, `back`, `forward`. |
11
+ | `useQueryParams` | Read/write `?key=value` URL state with typed coercion (`get`, `getNumber`, `getBoolean`, `set`, `remove`, `clear`). |
12
+ | `useBackOrFallback` | Smart "back" that falls back to a route when there's no in-app history. |
13
+ | `useUrlBuilder` | Pure URL/querystring assembly: `build`, `withCurrentParams`. |
14
+ | `useSmartLink` | Click + keyboard handlers that turn any element into a proper link (cmd-click, middle-click, Enter, Space). |
15
+ | `useIsActive` | `boolean` for "current pathname matches this href" — for nav-item highlighting. |
16
+ | `useQueryState` | Typed `useState`-style hook bound to ONE URL key (with parsers + `clearOnDefault`). |
17
+ | `useLocationProperty` | Subscribe to ONE derived field of `window.location` (avoids re-renders on unrelated fields). |
18
+ | `useRouter` | Convenience facade composing the above. |
19
+ | `RouterAdapterProvider` | Swap the navigation backend (e.g. Next.js's router). |
20
+ | `parseAsString` / `parseAsInteger` / `parseAsFloat` / `parseAsBoolean` / `parseAsIsoDate` / `parseAsStringEnum` / `parseAsArrayOf` / `parseAsJson` | Parser builders for `useQueryState`. Each has `.withDefault(value)`. |
21
+
22
+ ## Decomposition rationale
23
+
24
+ Each atomic hook subscribes to exactly what it needs. A component that only calls `navigate(...)` shouldn't re-render every time the querystring changes — `useNavigate` doesn't subscribe to location, so it doesn't. Same for `useUrlBuilder` (only re-renders on `search` change), `useQueryParams` (same), and so on.
25
+
26
+ `useRouter` exists for convenience and ergonomic familiarity. Use it when you want everything in one return; use the atomic hooks for fewer re-renders and better tree-shaking.
27
+
28
+ ## Adapter pattern
29
+
30
+ Default behavior uses `window.history.pushState` + `window.location` and works in any browser (Wails / Electron / Vite / CRA — nothing to mount, it just works).
31
+
32
+ For Next.js — mount the built-in adapter once near the root. It bridges every hook to `next/navigation` so server components, route loaders, and prefetch fire correctly:
33
+
34
+ ```tsx
35
+ // app/[locale]/layout.tsx (or wherever your client provider stack lives)
36
+ import { NextRouterAdapter } from '@djangocfg/ui-core/adapters/nextjs';
37
+
38
+ <I18nProvider locale={locale} messages={messages}>
39
+ <NextRouterAdapter>
40
+ <AppLayout>{children}</AppLayout>
41
+ </NextRouterAdapter>
42
+ </I18nProvider>
43
+ ```
44
+
45
+ `next` is an **optional peer dependency** — the package never imports from `next/*` from the main entry. The Next adapter lives behind the `/adapters/nextjs` sub-path entry, so non-Next consumers don't pull `next` into their bundle.
46
+
47
+ For other routers (TanStack Router, wouter, Remix, custom transports) — write a ~20-line custom adapter:
48
+
49
+ ```tsx
50
+ 'use client';
51
+
52
+ import { useMemo } from 'react';
53
+ import { RouterAdapterProvider, type RouterAdapter } from '@djangocfg/ui-core/hooks';
54
+
55
+ export function MyRouterAdapter({ children }: { children: React.ReactNode }) {
56
+ const myRouter = useMyRouter();
57
+ const adapter = useMemo<RouterAdapter>(() => ({
58
+ push: (url) => myRouter.push(url),
59
+ replace: (url) => myRouter.replace(url),
60
+ back: () => myRouter.back(),
61
+ forward: () => myRouter.forward(),
62
+ getLocation: () => ({
63
+ pathname: window.location.pathname,
64
+ search: window.location.search,
65
+ hash: window.location.hash,
66
+ }),
67
+ }), [myRouter]);
68
+
69
+ return <RouterAdapterProvider value={adapter}>{children}</RouterAdapterProvider>;
70
+ }
71
+ ```
72
+
73
+ **Adapter contract gotcha:** custom `push` / `replace` implementations should ultimately route through `window.history.pushState` (Next does, so does react-router). If yours doesn't, dispatch `'djc:navigate'` after each navigation manually so `useLocation` re-reads.
74
+
75
+ ## `useQueryState` (typed URL state)
76
+
77
+ Bound to one query key with a typed parser. Inspired by `nuqs` but framework-agnostic via the same adapter context.
78
+
79
+ ```tsx
80
+ import {
81
+ useQueryState,
82
+ parseAsInteger,
83
+ parseAsStringEnum,
84
+ } from '@djangocfg/ui-core/hooks';
85
+
86
+ const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
87
+ const [tab, setTab] = useQueryState('tab', parseAsStringEnum(['list', 'grid']).withDefault('list'));
88
+
89
+ setPage((prev) => prev + 1); // functional updater, just like useState
90
+ setPage(null); // clears the key from the URL
91
+ ```
92
+
93
+ `clearOnDefault` (default `true`) drops the key from the URL when the value equals the parser default — keeps URLs short. Pass `{ clearOnDefault: false }` to keep explicit `?page=1` for shareable links.
94
+
95
+ Parsers: `parseAsString`, `parseAsInteger`, `parseAsFloat`, `parseAsBoolean`, `parseAsIsoDate`, `parseAsStringEnum([...])`, `parseAsArrayOf(item)`, `parseAsJson<T>()`. Each has `.withDefault(value)`. Build your own by satisfying the `QueryParser<T>` interface — it's three functions (`parse`, `serialize`, `eq`).
96
+
97
+ ## How `useLocation` knows about `pushState`
98
+
99
+ Browsers don't fire `popstate` for programmatic `pushState` / `replaceState`. We monkey-patch both methods once (idempotent, module-level guard) on first mount and dispatch a custom `'djc:navigate'` event after each call. Anyone calling history APIs anywhere in the page will trigger an update — including the consumer's own router, third-party scripts, and our default adapter.
100
+
101
+ ## SSR
102
+
103
+ - All hooks return safe defaults on the server (`pathname: '/'`, etc.).
104
+ - `useSyncExternalStore`'s `getServerSnapshot` is wired up correctly.
105
+ - Mutating methods (`push`, `replace`, `navigate`, `navigateExternal`) no-op when `window` is undefined.
106
+ - No hydration mismatches — first client render reads real `window.location` after mount via `useSyncExternalStore`'s `getSnapshot`.
107
+
108
+ ## Trade-offs vs. `next/navigation`
109
+
110
+ | Concern | `next/navigation` | This library |
111
+ | --- | --- | --- |
112
+ | Server components fire | yes (built-in) | only if you mount the Next adapter |
113
+ | Pending state (`useTransition`) | bundled in | wrap calls yourself |
114
+ | Locale-prefix handling | yes | no — wrap if needed |
115
+ | Route matching / dynamic segments | yes | no — out of scope |
116
+ | Works outside Next | no | yes — anywhere React runs |
117
+ | `<Link>` component | yes | not yet (use `useSmartLink` on any element) |
118
+
119
+ Out of scope: locale prefixes, route matching, dynamic segments, transitions, `<Link>`. Add them in consumer code or in higher-level packages.
@@ -0,0 +1,139 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Router adapter — pluggable navigation backend.
5
+ *
6
+ * WHY:
7
+ * `@djangocfg/ui-core` is framework-agnostic, but consumers using Next.js,
8
+ * TanStack Router, wouter, etc. need SPA navigations to flow through their
9
+ * own router so server components / data loaders / route guards fire
10
+ * correctly. The adapter pattern lets the consumer swap the implementation
11
+ * without changing call-sites inside library components.
12
+ *
13
+ * Default behavior uses `window.history.pushState` + `window.location` (works
14
+ * in any browser, zero deps). When no provider is mounted, the default is
15
+ * silently used.
16
+ *
17
+ * @example
18
+ * // In a Next.js app:
19
+ * import { useRouter as useNextRouter } from 'next/navigation';
20
+ * import { RouterAdapterProvider } from '@djangocfg/ui-core/hooks';
21
+ *
22
+ * function NextAdapter({ children }: { children: React.ReactNode }) {
23
+ * const next = useNextRouter();
24
+ * const adapter = useMemo(() => ({
25
+ * push: (url: string) => next.push(url),
26
+ * replace: (url: string) => next.replace(url),
27
+ * back: () => next.back(),
28
+ * forward: () => next.forward(),
29
+ * getLocation: () => ({
30
+ * pathname: window.location.pathname,
31
+ * search: window.location.search,
32
+ * hash: window.location.hash,
33
+ * }),
34
+ * }), [next]);
35
+ * return <RouterAdapterProvider value={adapter}>{children}</RouterAdapterProvider>;
36
+ * }
37
+ */
38
+
39
+ import { createContext, useContext, useMemo, type ReactNode } from 'react';
40
+
41
+ /**
42
+ * Snapshot of the current URL location.
43
+ * Mirrors the parts of `window.location` we actually use.
44
+ */
45
+ export interface RouterLocation {
46
+ pathname: string;
47
+ search: string;
48
+ hash: string;
49
+ }
50
+
51
+ /**
52
+ * Pluggable navigation backend. Implementations must be SSR-safe
53
+ * (mutations should no-op when `typeof window === 'undefined'`).
54
+ */
55
+ export interface RouterAdapter {
56
+ /** Push a new entry onto the history stack. */
57
+ push: (url: string) => void;
58
+ /** Replace the current history entry. */
59
+ replace: (url: string) => void;
60
+ /** Go back one entry. */
61
+ back: () => void;
62
+ /** Go forward one entry. */
63
+ forward: () => void;
64
+ /** Read the current location (synchronous). */
65
+ getLocation: () => RouterLocation;
66
+ }
67
+
68
+ const SSR_LOCATION: RouterLocation = Object.freeze({
69
+ pathname: '/',
70
+ search: '',
71
+ hash: '',
72
+ });
73
+
74
+ /**
75
+ * Default adapter — uses History API + window.location.
76
+ * No-ops on the server. Triggers our internal `djc:navigate` event so
77
+ * `useLocation` reflects the change (see `useLocation.ts`).
78
+ */
79
+ export const defaultAdapter: RouterAdapter = Object.freeze({
80
+ push(url: string) {
81
+ if (typeof window === 'undefined') return;
82
+ window.history.pushState(null, '', url);
83
+ },
84
+ replace(url: string) {
85
+ if (typeof window === 'undefined') return;
86
+ window.history.replaceState(null, '', url);
87
+ },
88
+ back() {
89
+ if (typeof window === 'undefined') return;
90
+ window.history.back();
91
+ },
92
+ forward() {
93
+ if (typeof window === 'undefined') return;
94
+ window.history.forward();
95
+ },
96
+ getLocation(): RouterLocation {
97
+ if (typeof window === 'undefined') return SSR_LOCATION;
98
+ return {
99
+ pathname: window.location.pathname,
100
+ search: window.location.search,
101
+ hash: window.location.hash,
102
+ };
103
+ },
104
+ });
105
+
106
+ /**
107
+ * React context carrying the active adapter. `null` means "use default".
108
+ * Kept intentionally nullable so we don't burn a Provider for the default case.
109
+ */
110
+ export const RouterAdapterContext = createContext<RouterAdapter | null>(null);
111
+
112
+ export interface RouterAdapterProviderProps {
113
+ /** Adapter implementation. Will be used by every router hook in subtree. */
114
+ value: RouterAdapter;
115
+ children: ReactNode;
116
+ }
117
+
118
+ /**
119
+ * Wrap a subtree to override the navigation backend used by router hooks.
120
+ */
121
+ export function RouterAdapterProvider({ value, children }: RouterAdapterProviderProps) {
122
+ // Memoizing here is the consumer's job (the value usually comes from another hook).
123
+ // We don't double-memo — that just adds noise.
124
+ return (
125
+ <RouterAdapterContext.Provider value={value}>
126
+ {children}
127
+ </RouterAdapterContext.Provider>
128
+ );
129
+ }
130
+
131
+ /**
132
+ * Read the active router adapter. Returns the default History API adapter
133
+ * if no provider is mounted.
134
+ */
135
+ export function useRouterAdapter(): RouterAdapter {
136
+ const ctx = useContext(RouterAdapterContext);
137
+ // useMemo so consumers can put the returned value in deps without churn.
138
+ return useMemo(() => ctx ?? defaultAdapter, [ctx]);
139
+ }
@@ -0,0 +1,5 @@
1
+ // Adapters live behind sub-path imports so framework-specific peer
2
+ // deps (next, react-router, etc.) load only when the consumer asks
3
+ // for them. Do NOT re-export adapters from the package barrel — that
4
+ // would force every consumer to resolve all peer deps.
5
+ export type {} from '../adapter';
@@ -0,0 +1,100 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Next.js adapter — bridges our framework-agnostic router hooks
5
+ * to `next/navigation` so server components, route loaders, and
6
+ * `prefetch` all fire correctly.
7
+ *
8
+ * USAGE:
9
+ * Mount once near the root of the App Router tree, e.g. inside
10
+ * the locale layout's client provider stack:
11
+ *
12
+ * ```tsx
13
+ * import { NextRouterAdapter } from '@djangocfg/ui-core/adapters/nextjs';
14
+ *
15
+ * <I18nProvider locale={locale} messages={messages}>
16
+ * <NextRouterAdapter>
17
+ * <AppLayout>{children}</AppLayout>
18
+ * </NextRouterAdapter>
19
+ * </I18nProvider>
20
+ * ```
21
+ *
22
+ * Without this provider, our hooks fall back to the History API
23
+ * adapter — which works in plain SPAs (Wails, Electron, Vite, CRA)
24
+ * but in Next.js means: no server component refetch on navigation,
25
+ * no route loader, no prefetch. So always mount it in Next apps.
26
+ *
27
+ * PEER DEPENDENCY:
28
+ * `next` is an OPTIONAL peer of @djangocfg/ui-core. The base package
29
+ * never imports from `next/*`. This sub-path entry does — so it only
30
+ * loads when the consumer explicitly imports `/adapters/nextjs`.
31
+ * Wails / Electron consumers: don't import this file and `next` is
32
+ * never resolved.
33
+ */
34
+
35
+ import { useMemo, type ReactNode } from 'react';
36
+ import { useRouter as useNextRouter } from 'next/navigation';
37
+
38
+ import {
39
+ RouterAdapterProvider,
40
+ type RouterAdapter,
41
+ type RouterLocation,
42
+ } from '../adapter';
43
+
44
+ const SSR_LOCATION: RouterLocation = Object.freeze({
45
+ pathname: '/',
46
+ search: '',
47
+ hash: '',
48
+ });
49
+
50
+ export interface NextRouterAdapterProps {
51
+ children: ReactNode;
52
+ /**
53
+ * Pass `scroll: false` to every Next router call by default.
54
+ * Useful for filter/pagination apps where the page should never
55
+ * jump on URL changes. Default: undefined (use Next's own default).
56
+ */
57
+ defaultScroll?: boolean;
58
+ }
59
+
60
+ /**
61
+ * Wraps a subtree so all `@djangocfg/ui-core/hooks` router calls go
62
+ * through Next's App Router. Server components, loaders, and prefetch
63
+ * keep working as if you used `next/navigation` directly.
64
+ */
65
+ export function NextRouterAdapter({
66
+ children,
67
+ defaultScroll,
68
+ }: NextRouterAdapterProps) {
69
+ const next = useNextRouter();
70
+
71
+ const adapter = useMemo<RouterAdapter>(
72
+ () => ({
73
+ push(url) {
74
+ next.push(url, defaultScroll === undefined ? undefined : { scroll: defaultScroll });
75
+ },
76
+ replace(url) {
77
+ next.replace(url, defaultScroll === undefined ? undefined : { scroll: defaultScroll });
78
+ },
79
+ back() {
80
+ next.back();
81
+ },
82
+ forward() {
83
+ next.forward();
84
+ },
85
+ getLocation() {
86
+ if (typeof window === 'undefined') return SSR_LOCATION;
87
+ return {
88
+ pathname: window.location.pathname,
89
+ search: window.location.search,
90
+ hash: window.location.hash,
91
+ };
92
+ },
93
+ }),
94
+ [next, defaultScroll]
95
+ );
96
+
97
+ return (
98
+ <RouterAdapterProvider value={adapter}>{children}</RouterAdapterProvider>
99
+ );
100
+ }
@@ -0,0 +1,90 @@
1
+ // ============================================================================
2
+ // Router hooks — framework-agnostic navigation primitives.
3
+ // See ./README.md for design rationale and Next.js adapter example.
4
+ // ============================================================================
5
+
6
+ 'use client';
7
+
8
+ // Adapter (context + provider + default + types)
9
+ export {
10
+ RouterAdapterContext,
11
+ RouterAdapterProvider,
12
+ defaultAdapter,
13
+ useRouterAdapter,
14
+ } from './adapter';
15
+ export type {
16
+ RouterAdapter,
17
+ RouterAdapterProviderProps,
18
+ RouterLocation,
19
+ } from './adapter';
20
+
21
+ // useLocation (+ per-property subscription)
22
+ export {
23
+ useLocation,
24
+ useLocationProperty,
25
+ NAVIGATE_EVENT,
26
+ PUSH_STATE_EVENT,
27
+ REPLACE_STATE_EVENT,
28
+ } from './useLocation';
29
+ export type { LocationSnapshot } from './useLocation';
30
+
31
+ // useNavigate
32
+ export { useNavigate } from './useNavigate';
33
+ export type { NavigateOptions, UseNavigateReturn } from './useNavigate';
34
+
35
+ // useQueryParams
36
+ export { useQueryParams } from './useQueryParams';
37
+ export type {
38
+ QueryParamsSnapshot,
39
+ QueryParamValue,
40
+ QueryParamUpdates,
41
+ SetQueryParamsOptions,
42
+ UseQueryParamsReturn,
43
+ } from './useQueryParams';
44
+
45
+ // useBackOrFallback
46
+ export { useBackOrFallback } from './useBackOrFallback';
47
+ export type { UseBackOrFallbackReturn } from './useBackOrFallback';
48
+
49
+ // useUrlBuilder (+ pure helpers)
50
+ export { useUrlBuilder, buildUrl, buildQueryString } from './useUrlBuilder';
51
+ export type {
52
+ QueryValue,
53
+ QueryParamsInput,
54
+ UseUrlBuilderReturn,
55
+ } from './useUrlBuilder';
56
+
57
+ // useSmartLink
58
+ export { useSmartLink } from './useSmartLink';
59
+ export type {
60
+ UseSmartLinkOptions,
61
+ SmartLinkHandlers,
62
+ } from './useSmartLink';
63
+
64
+ // useIsActive
65
+ export { useIsActive } from './useIsActive';
66
+ export type { UseIsActiveOptions } from './useIsActive';
67
+
68
+ // useQueryState (typed single-key URL state, nuqs-style)
69
+ export { useQueryState } from './useQueryState';
70
+ export type {
71
+ UseQueryStateOptions,
72
+ QueryStateUpdater,
73
+ } from './useQueryState';
74
+
75
+ // Parsers (typed marshalling between URL strings and JS values)
76
+ export {
77
+ parseAsString,
78
+ parseAsInteger,
79
+ parseAsFloat,
80
+ parseAsBoolean,
81
+ parseAsIsoDate,
82
+ parseAsStringEnum,
83
+ parseAsArrayOf,
84
+ parseAsJson,
85
+ } from './parsers';
86
+ export type { QueryParser, QueryParserBuilder } from './parsers';
87
+
88
+ // useRouter (composite facade)
89
+ export { useRouter } from './useRouter';
90
+ export type { UseRouterReturn } from './useRouter';