@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.
- package/README.md +127 -26
- package/package.json +16 -4
- package/src/components/feedback/sonner/index.tsx +1 -1
- package/src/components/forms/button/index.tsx +21 -5
- package/src/components/forms/button-download/index.tsx +1 -1
- package/src/components/forms/input/index.tsx +1 -1
- package/src/components/forms/otp/index.tsx +1 -1
- package/src/components/forms/slider/index.tsx +1 -1
- package/src/components/forms/textarea/index.tsx +1 -1
- package/src/components/index.ts +2 -0
- package/src/components/layout/sticky/index.tsx +1 -1
- package/src/components/navigation/accordion/index.tsx +1 -1
- package/src/components/navigation/dropdown-menu/index.tsx +3 -2
- package/src/components/navigation/link/Link.tsx +124 -0
- package/src/components/navigation/link/LinkContext.tsx +52 -0
- package/src/components/navigation/link/index.ts +8 -0
- package/src/components/navigation/menubar/index.tsx +3 -2
- package/src/components/navigation/navigation-menu/index.tsx +2 -1
- package/src/components/navigation/tabs/index.tsx +1 -1
- package/src/components/overlay/responsive-sheet/index.tsx +1 -1
- package/src/components/select/combobox.tsx +1 -1
- package/src/components/select/multi-select.tsx +1 -1
- package/src/components/specialized/image-with-fallback/index.tsx +1 -1
- package/src/hooks/debug/index.ts +3 -0
- package/src/hooks/device/index.ts +7 -0
- package/src/hooks/dom/index.ts +12 -0
- package/src/hooks/{useBodyScrollLock.ts → dom/useBodyScrollLock.ts} +1 -1
- package/src/hooks/{useCopy.ts → dom/useCopy.ts} +1 -1
- package/src/hooks/dom/useScroll.ts +322 -0
- package/src/hooks/events/index.ts +3 -0
- package/src/hooks/feedback/index.ts +3 -0
- package/src/hooks/hotkey/index.ts +4 -0
- package/src/hooks/index.ts +82 -26
- package/src/hooks/media/index.ts +5 -0
- package/src/hooks/router/README.md +121 -0
- package/src/hooks/router/adapter.tsx +139 -0
- package/src/hooks/router/adapters/index.ts +5 -0
- package/src/hooks/router/adapters/nextjs.tsx +140 -0
- package/src/hooks/router/index.ts +90 -0
- package/src/hooks/router/parsers.ts +154 -0
- package/src/hooks/router/useBackOrFallback.ts +145 -0
- package/src/hooks/router/useIsActive.ts +60 -0
- package/src/hooks/router/useLocation.ts +163 -0
- package/src/hooks/router/useNavigate.ts +96 -0
- package/src/hooks/router/useQueryParams.ts +262 -0
- package/src/hooks/router/useQueryState.ts +106 -0
- package/src/hooks/router/useRouter.ts +81 -0
- package/src/hooks/router/useSmartLink.ts +157 -0
- package/src/hooks/router/useUrlBuilder.ts +118 -0
- package/src/hooks/state/index.ts +8 -0
- package/src/hooks/theme/index.ts +4 -0
- package/src/hooks/time/index.ts +4 -0
- package/src/lib/dialog-service/dialogs/AlertDialogUI.tsx +1 -1
- package/src/lib/dialog-service/dialogs/ConfirmDialogUI.tsx +1 -1
- package/src/lib/dialog-service/dialogs/PromptDialogUI.tsx +1 -1
- package/src/styles/palette/useThemePalette.ts +1 -1
- /package/src/hooks/{useDebugTools.ts → debug/useDebugTools.ts} +0 -0
- /package/src/hooks/{useBrowserDetect.ts → device/useBrowserDetect.ts} +0 -0
- /package/src/hooks/{useDeviceDetect.ts → device/useDeviceDetect.ts} +0 -0
- /package/src/hooks/{useShortcutModLabel.ts → device/useShortcutModLabel.ts} +0 -0
- /package/src/hooks/{useImageLoader.ts → dom/useImageLoader.ts} +0 -0
- /package/src/hooks/{useEventsBus.ts → events/useEventsBus.ts} +0 -0
- /package/src/hooks/{useToast.ts → feedback/useToast.ts} +0 -0
- /package/src/hooks/{useHotkey.ts → hotkey/useHotkey.ts} +0 -0
- /package/src/hooks/{useMediaQuery.ts → media/useMediaQuery.ts} +0 -0
- /package/src/hooks/{useMobile.tsx → media/useMobile.tsx} +0 -0
- /package/src/hooks/{useDebounce.ts → state/useDebounce.ts} +0 -0
- /package/src/hooks/{useDebouncedCallback.ts → state/useDebouncedCallback.ts} +0 -0
- /package/src/hooks/{useLocalStorage.ts → state/useLocalStorage.ts} +0 -0
- /package/src/hooks/{useSessionStorage.ts → state/useSessionStorage.ts} +0 -0
- /package/src/hooks/{useStoredValue.ts → state/useStoredValue.ts} +0 -0
- /package/src/hooks/{useResolvedTheme.ts → theme/useResolvedTheme.ts} +0 -0
- /package/src/hooks/{useCountdown.ts → time/useCountdown.ts} +0 -0
package/README.md
CHANGED
|
@@ -83,28 +83,62 @@ 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 (
|
|
86
|
+
## Hooks (30+)
|
|
87
|
+
|
|
88
|
+
Hooks are organized by domain inside the package (`src/hooks/<group>/`).
|
|
89
|
+
Public import path is the single barrel:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { useIsMobile, useScroll, useNavigate } from '@djangocfg/ui-core/hooks';
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### `dom/` — DOM & viewport
|
|
87
96
|
|
|
88
97
|
| Hook | Description |
|
|
89
98
|
|------|-------------|
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
|
99
|
-
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
|
99
|
+
| `useScroll(target?)` | Reactive `{ x, y, direction, isScrolling }` for window or any scrollable element. `useSyncExternalStore` + module-level shared store + rAF throttle + passive listener. |
|
|
100
|
+
| `useScrollPosition(target?)` / `useScrollDirection(target?)` / `useIsScrolling(target?)` | Single-field variants — re-render only when their slice changes. |
|
|
101
|
+
| `useBodyScrollLock(locked)` | Lock body scroll while `locked=true`; counter-based (multi-consumer safe), iOS-safe via `position: fixed` fallback. |
|
|
102
|
+
| `useCopy` | Copy to clipboard. |
|
|
103
|
+
| `useImageLoader` | Image loading state. |
|
|
104
|
+
|
|
105
|
+
### `media/` — viewport size
|
|
106
|
+
|
|
107
|
+
| Hook | Description |
|
|
108
|
+
|------|-------------|
|
|
109
|
+
| `useMediaQuery(query)` | Raw media query — pass any CSS query string. Exports `BREAKPOINTS` constants (Tailwind v4 defaults). |
|
|
110
|
+
| `useIsPhone()` | `< 640px` — phones only. |
|
|
111
|
+
| `useIsMobile()` | `< 768px` — phones + small tablets. |
|
|
112
|
+
| `useIsTabletOrBelow()` | `< 1024px` — phones + tablets. |
|
|
113
|
+
|
|
114
|
+
### `state/` — state primitives
|
|
115
|
+
|
|
116
|
+
| Hook | Description |
|
|
117
|
+
|------|-------------|
|
|
118
|
+
| `useDebounce` | Debounce values. |
|
|
119
|
+
| `useDebouncedCallback` | Debounced callbacks. |
|
|
120
|
+
| `useLocalStorage` / `useSessionStorage` | Type-safe wrappers with TTL. |
|
|
121
|
+
| `useStoredValue` | Unified API over local/session storage. |
|
|
122
|
+
|
|
123
|
+
### `device/` — environment detection
|
|
124
|
+
|
|
125
|
+
| Hook | Description |
|
|
126
|
+
|------|-------------|
|
|
127
|
+
| `useBrowserDetect` | Browser detection (Chrome, Safari, in-app browsers, etc.). |
|
|
128
|
+
| `useDeviceDetect` | Device detection (mobile, tablet, desktop, OS, etc.). |
|
|
129
|
+
| `useShortcutModLabel()` | Returns `⌘` or `Ctrl` for shortcut hints (Apple vs Windows/Linux); pairs with `metaKey \|\| ctrlKey` handlers. |
|
|
130
|
+
|
|
131
|
+
### Other groups
|
|
132
|
+
|
|
133
|
+
| Group | Hooks |
|
|
134
|
+
|-------|-------|
|
|
135
|
+
| `feedback/` | `useToast`, `toast` (Sonner). |
|
|
136
|
+
| `theme/` | `useResolvedTheme` — current resolved theme (light/dark/system). |
|
|
137
|
+
| `time/` | `useCountdown`, `useCountdownFromSeconds`. |
|
|
138
|
+
| `events/` | `useEventListener`, `events` (PubSub bus). |
|
|
139
|
+
| `hotkey/` | `useHotkey`, `HotkeysProvider` (react-hotkeys-hook). |
|
|
140
|
+
| `debug/` | `useDebugTools`. |
|
|
141
|
+
| `router/` | See [Router Hooks](#router-hooks) below — its own subsection. |
|
|
108
142
|
|
|
109
143
|
```tsx
|
|
110
144
|
import { useMediaQuery, useIsPhone, useIsMobile, BREAKPOINTS } from '@djangocfg/ui-core/hooks'
|
|
@@ -116,8 +150,61 @@ const isMobile = useIsMobile() // < 768px
|
|
|
116
150
|
// custom with constants
|
|
117
151
|
const isNarrow = useMediaQuery(`(max-width: ${BREAKPOINTS.sm - 1}px)`)
|
|
118
152
|
const isDark = useMediaQuery('(prefers-color-scheme: dark)')
|
|
153
|
+
|
|
154
|
+
// scroll snapshot — direction-aware navbar
|
|
155
|
+
const { direction, isScrolling } = useScroll()
|
|
156
|
+
const hideNav = direction === 'down' && isScrolling
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Router Hooks
|
|
160
|
+
|
|
161
|
+
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).
|
|
162
|
+
|
|
163
|
+
| Hook | Purpose |
|
|
164
|
+
|------|---------|
|
|
165
|
+
| `useLocation()` | Reactive `{ pathname, search, hash, href }`. |
|
|
166
|
+
| `useLocationProperty(get, getSsr)` | Subscribe to ONE field — skip re-renders on unrelated changes. |
|
|
167
|
+
| `useNavigate()` | `navigate`, `navigateExternal`, `push`, `replace`, `back`, `forward`. |
|
|
168
|
+
| `useQueryParams()` | Record-style read/write of `?key=value` URL state. |
|
|
169
|
+
| `useQueryState(key, parser)` | Typed `useState`-style hook bound to one URL key (with `clearOnDefault`). |
|
|
170
|
+
| `useBackOrFallback()` | Smart Back that falls back to a route when there's no in-app history. |
|
|
171
|
+
| `useUrlBuilder()` | Pure URL assembly: `build`, `withCurrentParams`. |
|
|
172
|
+
| `useSmartLink(href)` | Make any element a link (cmd-click, middle-click, Enter, Space). |
|
|
173
|
+
| `useIsActive(href)` | Boolean for nav-item highlighting. |
|
|
174
|
+
| `useRouter()` | Convenience facade composing everything. |
|
|
175
|
+
| `RouterAdapterProvider` | Swap the navigation backend. |
|
|
176
|
+
| `parseAsString` / `parseAsInteger` / `parseAsFloat` / `parseAsBoolean` / `parseAsIsoDate` / `parseAsStringEnum` / `parseAsArrayOf` / `parseAsJson` | Parsers for `useQueryState`. Each has `.withDefault(value)`. |
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import {
|
|
180
|
+
useNavigate,
|
|
181
|
+
useQueryState,
|
|
182
|
+
parseAsInteger,
|
|
183
|
+
} from '@djangocfg/ui-core/hooks';
|
|
184
|
+
|
|
185
|
+
const { navigate } = useNavigate();
|
|
186
|
+
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
|
|
187
|
+
|
|
188
|
+
navigate('/products');
|
|
189
|
+
setPage((p) => p + 1); // ?page=2 — `page=1` is dropped (clearOnDefault)
|
|
119
190
|
```
|
|
120
191
|
|
|
192
|
+
### Next.js adapter
|
|
193
|
+
|
|
194
|
+
In Next apps, mount both adapters once near the root so navigation flows through `next/navigation` (server components + prefetch keep working) and `<Link>` delegates to `next/link`:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { NextRouterAdapter, NextLinkProvider } from '@djangocfg/ui-core/adapters/nextjs';
|
|
198
|
+
|
|
199
|
+
<NextRouterAdapter>
|
|
200
|
+
<NextLinkProvider>
|
|
201
|
+
<App />
|
|
202
|
+
</NextLinkProvider>
|
|
203
|
+
</NextRouterAdapter>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`next` is an **optional peer dependency** — Wails / Electron / Vite consumers don't pull it in. `@djangocfg/layouts/BaseApp` mounts both adapters automatically.
|
|
207
|
+
|
|
121
208
|
## Theme Palette Hooks
|
|
122
209
|
|
|
123
210
|
Hooks for accessing theme colors from CSS variables (useful for Canvas, SVG, charts, diagrams, etc.):
|
|
@@ -290,7 +377,8 @@ import '@djangocfg/ui-core/styles/globals';
|
|
|
290
377
|
|------|---------|
|
|
291
378
|
| `@djangocfg/ui-core` | All components & hooks |
|
|
292
379
|
| `@djangocfg/ui-core/components` | Components only |
|
|
293
|
-
| `@djangocfg/ui-core/hooks` | Hooks only |
|
|
380
|
+
| `@djangocfg/ui-core/hooks` | Hooks only (incl. router hooks) |
|
|
381
|
+
| `@djangocfg/ui-core/adapters/nextjs` | `<NextRouterAdapter>` + `<NextLinkProvider>` for Next.js apps (optional peer: `next`) |
|
|
294
382
|
| `@djangocfg/ui-core/lib` | Utilities (cn, etc.) |
|
|
295
383
|
| `@djangocfg/ui-core/lib/dialog-service` | Dialog service |
|
|
296
384
|
| `@djangocfg/ui-core/utils` | Runtime utilities (emitRuntimeError) |
|
|
@@ -311,18 +399,31 @@ try {
|
|
|
311
399
|
}
|
|
312
400
|
```
|
|
313
401
|
|
|
402
|
+
## Links
|
|
403
|
+
|
|
404
|
+
`<Link>` and `<ButtonLink>` ship in `ui-core` itself — no Next.js required.
|
|
405
|
+
Default behavior renders `<a>` and routes clicks through `useNavigate`.
|
|
406
|
+
|
|
407
|
+
In Next.js apps, mount `NextLinkProvider` (from `@djangocfg/ui-core/adapters/nextjs`)
|
|
408
|
+
near the root and the same components delegate to `next/link` automatically.
|
|
409
|
+
`@djangocfg/layouts/BaseApp` does this for you.
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
import { Link, ButtonLink } from '@djangocfg/ui-core/components';
|
|
413
|
+
|
|
414
|
+
<Link href="/about">About</Link>
|
|
415
|
+
<ButtonLink href="/docs" variant="outline">Docs</ButtonLink>
|
|
416
|
+
```
|
|
417
|
+
|
|
314
418
|
## What's NOT included (use ui-nextjs)
|
|
315
419
|
|
|
316
420
|
These features require Next.js or browser storage APIs:
|
|
317
421
|
|
|
318
|
-
- `Sidebar` —
|
|
319
|
-
- `Breadcrumb`, `BreadcrumbNavigation` —
|
|
320
|
-
- `
|
|
321
|
-
- `Pagination`, `SSRPagination` — uses next/link
|
|
322
|
-
- `DropdownMenu` — uses next/link
|
|
422
|
+
- `Sidebar` — `'use client'` heavy, lives in ui-nextjs
|
|
423
|
+
- `Breadcrumb`, `BreadcrumbNavigation` — same
|
|
424
|
+
- `Pagination`, `SSRPagination` — same
|
|
323
425
|
- `DownloadButton` — uses localStorage
|
|
324
426
|
- `useTheme` — uses next-themes
|
|
325
|
-
- `useQueryParams`, `useCfgRouter` — uses next/router
|
|
326
427
|
|
|
327
428
|
## Requirements
|
|
328
429
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.297",
|
|
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.
|
|
94
|
+
"@djangocfg/i18n": "^2.1.297",
|
|
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.
|
|
162
|
+
"@djangocfg/i18n": "^2.1.297",
|
|
152
163
|
"@djangocfg/playground": "workspace:*",
|
|
153
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
164
|
+
"@djangocfg/typescript-config": "^2.1.297",
|
|
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": {
|
|
@@ -7,6 +7,7 @@ import * as React from 'react';
|
|
|
7
7
|
import { Slot } from '@radix-ui/react-slot';
|
|
8
8
|
|
|
9
9
|
import { cn } from '../../../lib/utils';
|
|
10
|
+
import { Link } from '../../navigation/link';
|
|
10
11
|
|
|
11
12
|
const buttonVariants = cva(
|
|
12
13
|
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
@@ -101,24 +102,39 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
101
102
|
)
|
|
102
103
|
Button.displayName = "Button"
|
|
103
104
|
|
|
104
|
-
|
|
105
|
+
/**
|
|
106
|
+
* ButtonLink — anchor styled as a Button.
|
|
107
|
+
*
|
|
108
|
+
* Renders the framework-agnostic `<Link>` underneath, so SPA navigation,
|
|
109
|
+
* cmd-click, and the Next.js adapter (if mounted) all work out of the box.
|
|
110
|
+
* Drop-in replacement for the previous native-`<a>` version.
|
|
111
|
+
*/
|
|
105
112
|
export interface ButtonLinkProps
|
|
106
|
-
extends React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
113
|
+
extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>,
|
|
107
114
|
VariantProps<typeof buttonVariants> {
|
|
108
115
|
href: string
|
|
116
|
+
/** Use replaceState instead of pushState. Default: false. */
|
|
117
|
+
replace?: boolean
|
|
118
|
+
/** Scroll to top after SPA navigation. Default: false. */
|
|
119
|
+
scroll?: boolean
|
|
120
|
+
/** Hint for the host framework's prefetcher (Next.js). */
|
|
121
|
+
prefetch?: boolean | null
|
|
109
122
|
}
|
|
110
123
|
|
|
111
124
|
const ButtonLink = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
|
112
|
-
({ className, variant, size, href, children, ...props }, ref) => {
|
|
125
|
+
({ className, variant, size, href, replace, scroll, prefetch, children, ...props }, ref) => {
|
|
113
126
|
return (
|
|
114
|
-
<
|
|
127
|
+
<Link
|
|
115
128
|
href={href}
|
|
129
|
+
replace={replace}
|
|
130
|
+
scroll={scroll}
|
|
131
|
+
prefetch={prefetch}
|
|
116
132
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
117
133
|
ref={ref}
|
|
118
134
|
{...props}
|
|
119
135
|
>
|
|
120
136
|
{children}
|
|
121
|
-
</
|
|
137
|
+
</Link>
|
|
122
138
|
)
|
|
123
139
|
}
|
|
124
140
|
)
|
|
@@ -5,7 +5,7 @@ import * as React from 'react';
|
|
|
5
5
|
|
|
6
6
|
import { Button, type ButtonProps } from '../../forms/button';
|
|
7
7
|
import { cn } from '../../../lib';
|
|
8
|
-
import { useLocalStorage } from '../../../hooks
|
|
8
|
+
import { useLocalStorage } from '../../../hooks';
|
|
9
9
|
|
|
10
10
|
// Token key used by the API client
|
|
11
11
|
const TOKEN_KEY = "auth_token"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
import { useStoredValue, type StorageType, type UseStoredValueOptions } from '../../../hooks
|
|
3
|
+
import { useStoredValue, type StorageType, type UseStoredValueOptions } from '../../../hooks';
|
|
4
4
|
import { cn } from '../../../lib/utils';
|
|
5
5
|
|
|
6
6
|
export interface InputProps extends React.ComponentProps<"input"> {
|
|
@@ -5,7 +5,7 @@ import * as React from 'react';
|
|
|
5
5
|
|
|
6
6
|
import { InputOTP, InputOTPGroup, InputOTPSlot } from '../input-otp';
|
|
7
7
|
import { cn } from '../../../lib';
|
|
8
|
-
import { useIsMobile } from '../../../hooks
|
|
8
|
+
import { useIsMobile } from '../../../hooks';
|
|
9
9
|
|
|
10
10
|
import { createPasteHandler, useSmartOTP } from './use-otp-input';
|
|
11
11
|
|
|
@@ -4,7 +4,7 @@ import * as React from 'react';
|
|
|
4
4
|
|
|
5
5
|
import * as SliderPrimitive from '@radix-ui/react-slider';
|
|
6
6
|
|
|
7
|
-
import { useStoredValue, type StorageType, type UseStoredValueOptions } from '../../../hooks
|
|
7
|
+
import { useStoredValue, type StorageType, type UseStoredValueOptions } from '../../../hooks';
|
|
8
8
|
import { cn } from '../../../lib/utils';
|
|
9
9
|
|
|
10
10
|
export interface SliderProps extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
import { useStoredValue, type StorageType, type UseStoredValueOptions } from '../../../hooks
|
|
3
|
+
import { useStoredValue, type StorageType, type UseStoredValueOptions } from '../../../hooks';
|
|
4
4
|
import { cn } from '../../../lib/utils';
|
|
5
5
|
|
|
6
6
|
export interface TextareaProps extends React.ComponentProps<"textarea"> {
|
package/src/components/index.ts
CHANGED
|
@@ -60,6 +60,8 @@ export { Tabs, TabsContent, TabsList, TabsTrigger } from './navigation/tabs';
|
|
|
60
60
|
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './navigation/accordion';
|
|
61
61
|
export { Collapsible, CollapsibleContent, CollapsibleTrigger } from './navigation/collapsible';
|
|
62
62
|
export { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from './navigation/command';
|
|
63
|
+
export { Link, LinkProvider, LinkComponentContext, useLinkComponent } from './navigation/link';
|
|
64
|
+
export type { LinkProps, LinkComponent, LinkComponentProps, LinkProviderProps } from './navigation/link';
|
|
63
65
|
|
|
64
66
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
65
67
|
// Layout
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
import * as React from 'react';
|
|
36
36
|
import StickyBox from 'react-sticky-box';
|
|
37
37
|
|
|
38
|
-
import { useIsMobile } from '../../../hooks
|
|
38
|
+
import { useIsMobile } from '../../../hooks';
|
|
39
39
|
|
|
40
40
|
export interface StickyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
41
41
|
/** Stick to bottom instead of top */
|
|
@@ -5,7 +5,7 @@ import * as React from 'react';
|
|
|
5
5
|
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
|
6
6
|
import { ChevronDownIcon } from '@radix-ui/react-icons';
|
|
7
7
|
|
|
8
|
-
import { useStoredValue, type StorageType, type UseStoredValueOptions } from '../../../hooks
|
|
8
|
+
import { useStoredValue, type StorageType, type UseStoredValueOptions } from '../../../hooks';
|
|
9
9
|
import { cn } from '../../../lib/utils';
|
|
10
10
|
|
|
11
11
|
type AccordionSingleProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Root> & {
|
|
@@ -5,6 +5,7 @@ import * as React from 'react';
|
|
|
5
5
|
|
|
6
6
|
import { cn } from '../../../lib';
|
|
7
7
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
|
8
|
+
import { Link } from '../link';
|
|
8
9
|
|
|
9
10
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
10
11
|
|
|
@@ -92,9 +93,9 @@ const DropdownMenuItem = React.forwardRef<
|
|
|
92
93
|
if (href) {
|
|
93
94
|
return (
|
|
94
95
|
<DropdownMenuPrimitive.Item asChild ref={ref} {...props}>
|
|
95
|
-
<
|
|
96
|
+
<Link href={href} className={classes}>
|
|
96
97
|
{children}
|
|
97
|
-
</
|
|
98
|
+
</Link>
|
|
98
99
|
</DropdownMenuPrimitive.Item>
|
|
99
100
|
)
|
|
100
101
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* <Link> — framework-agnostic link component.
|
|
5
|
+
*
|
|
6
|
+
* WHY:
|
|
7
|
+
* We want a single Link import everywhere, regardless of whether the
|
|
8
|
+
* host app is Next.js, Wails, Electron, Vite, or plain CRA. When a
|
|
9
|
+
* framework adapter is mounted (e.g. NextLinkProvider) the native
|
|
10
|
+
* component handles prefetch / locale / RSC wiring. Otherwise we
|
|
11
|
+
* render a plain <a> and route clicks through the active router
|
|
12
|
+
* adapter (which itself defaults to History API).
|
|
13
|
+
*
|
|
14
|
+
* Behavior parity with `<a>`:
|
|
15
|
+
* - Cmd/Ctrl/Shift+click and middle-click open a new tab natively
|
|
16
|
+
* (we let the browser handle modifier clicks — DON'T preventDefault).
|
|
17
|
+
* - External / non-internal hrefs (http://, mailto:, tel:, target=_blank)
|
|
18
|
+
* skip SPA nav entirely and behave as the browser would.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* import { Link } from '@djangocfg/ui-core/components';
|
|
22
|
+
* <Link href="/dashboard">Dashboard</Link>
|
|
23
|
+
* <Link href="/dashboard" replace scroll>Reset</Link>
|
|
24
|
+
* <Link href="https://example.com" target="_blank" rel="noopener noreferrer">External</Link>
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { forwardRef, useCallback, type AnchorHTMLAttributes, type MouseEvent, type ReactNode } from 'react';
|
|
28
|
+
import { useNavigate } from '../../../hooks/router/useNavigate';
|
|
29
|
+
import { useLinkComponent } from './LinkContext';
|
|
30
|
+
|
|
31
|
+
type AnchorProps = Omit<
|
|
32
|
+
AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
33
|
+
'href' | 'onClick'
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
export interface LinkProps extends AnchorProps {
|
|
37
|
+
href: string;
|
|
38
|
+
/** Use replaceState instead of pushState. Default: false. */
|
|
39
|
+
replace?: boolean;
|
|
40
|
+
/** Scroll to top after SPA navigation. Default: false. */
|
|
41
|
+
scroll?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Hint for the host framework's prefetcher (Next.js).
|
|
44
|
+
* Ignored by the agnostic fallback.
|
|
45
|
+
*/
|
|
46
|
+
prefetch?: boolean | null;
|
|
47
|
+
children?: ReactNode;
|
|
48
|
+
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** True for protocols / schemes the browser must handle natively. */
|
|
52
|
+
function isExternalHref(href: string): boolean {
|
|
53
|
+
// Allow same-origin absolute paths (`/foo`, `./foo`, `#hash`, `?q=1`).
|
|
54
|
+
if (href.startsWith('/') && !href.startsWith('//')) return false;
|
|
55
|
+
if (href.startsWith('#') || href.startsWith('?')) return false;
|
|
56
|
+
// Anything with a scheme (http:, https:, mailto:, tel:, ipfs:, …).
|
|
57
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href)) return true;
|
|
58
|
+
// `//example.com` protocol-relative.
|
|
59
|
+
if (href.startsWith('//')) return true;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* True when the click should be left to the browser:
|
|
65
|
+
* modifier keys, non-primary mouse button, or `target=_blank`.
|
|
66
|
+
*/
|
|
67
|
+
function shouldBypassClick(event: MouseEvent<HTMLAnchorElement>, target?: string): boolean {
|
|
68
|
+
if (event.defaultPrevented) return true;
|
|
69
|
+
if (event.button !== 0) return true;
|
|
70
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return true;
|
|
71
|
+
if (target && target !== '' && target !== '_self') return true;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
|
76
|
+
{ href, replace, scroll, prefetch, target, rel, onClick, children, ...rest },
|
|
77
|
+
ref
|
|
78
|
+
) {
|
|
79
|
+
const Adapter = useLinkComponent();
|
|
80
|
+
const { navigate } = useNavigate();
|
|
81
|
+
|
|
82
|
+
const handleClick = useCallback(
|
|
83
|
+
(event: MouseEvent<HTMLAnchorElement>) => {
|
|
84
|
+
onClick?.(event);
|
|
85
|
+
if (event.defaultPrevented) return;
|
|
86
|
+
if (isExternalHref(href)) return;
|
|
87
|
+
if (shouldBypassClick(event, target)) return;
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
navigate(href, { replace, scroll });
|
|
90
|
+
},
|
|
91
|
+
[onClick, href, target, navigate, replace, scroll]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (Adapter) {
|
|
95
|
+
return (
|
|
96
|
+
<Adapter
|
|
97
|
+
href={href}
|
|
98
|
+
replace={replace}
|
|
99
|
+
scroll={scroll}
|
|
100
|
+
prefetch={prefetch}
|
|
101
|
+
target={target}
|
|
102
|
+
rel={rel}
|
|
103
|
+
ref={ref}
|
|
104
|
+
onClick={onClick}
|
|
105
|
+
{...rest}
|
|
106
|
+
>
|
|
107
|
+
{children}
|
|
108
|
+
</Adapter>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<a
|
|
114
|
+
href={href}
|
|
115
|
+
target={target}
|
|
116
|
+
rel={rel}
|
|
117
|
+
ref={ref}
|
|
118
|
+
onClick={handleClick}
|
|
119
|
+
{...rest}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
</a>
|
|
123
|
+
);
|
|
124
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LinkContext
|
|
5
|
+
*
|
|
6
|
+
* Adapter slot for the framework's native link component (e.g. `next/link`).
|
|
7
|
+
* `<Link>` reads this context: when populated, it delegates rendering so
|
|
8
|
+
* the host framework's prefetching / locale handling kicks in. When empty,
|
|
9
|
+
* `<Link>` falls back to a pure `<a>` driven by `useNavigate`.
|
|
10
|
+
*
|
|
11
|
+
* Mount once near the root via the matching adapter
|
|
12
|
+
* (e.g. `NextLinkProvider` from `@djangocfg/ui-core/adapters/nextjs`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createContext, useContext, type ComponentType, type ReactNode, type Ref } from 'react';
|
|
16
|
+
|
|
17
|
+
export interface LinkComponentProps {
|
|
18
|
+
href: string;
|
|
19
|
+
replace?: boolean;
|
|
20
|
+
scroll?: boolean;
|
|
21
|
+
prefetch?: boolean | null;
|
|
22
|
+
className?: string;
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
target?: string;
|
|
25
|
+
rel?: string;
|
|
26
|
+
ref?: Ref<HTMLAnchorElement>;
|
|
27
|
+
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
|
28
|
+
[dataAttr: `data-${string}`]: unknown;
|
|
29
|
+
[ariaAttr: `aria-${string}`]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type LinkComponent = ComponentType<LinkComponentProps>;
|
|
33
|
+
|
|
34
|
+
export const LinkComponentContext = createContext<LinkComponent | null>(null);
|
|
35
|
+
|
|
36
|
+
export interface LinkProviderProps {
|
|
37
|
+
value: LinkComponent;
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function LinkProvider({ value, children }: LinkProviderProps) {
|
|
42
|
+
return (
|
|
43
|
+
<LinkComponentContext.Provider value={value}>
|
|
44
|
+
{children}
|
|
45
|
+
</LinkComponentContext.Provider>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Returns the host-framework Link if a provider is mounted, else `null`. */
|
|
50
|
+
export function useLinkComponent(): LinkComponent | null {
|
|
51
|
+
return useContext(LinkComponentContext);
|
|
52
|
+
}
|
|
@@ -5,6 +5,7 @@ import * as React from 'react';
|
|
|
5
5
|
|
|
6
6
|
import { cn } from '../../../lib';
|
|
7
7
|
import * as MenubarPrimitive from '@radix-ui/react-menubar';
|
|
8
|
+
import { Link } from '../link';
|
|
8
9
|
|
|
9
10
|
function MenubarMenu({
|
|
10
11
|
...props
|
|
@@ -144,9 +145,9 @@ const MenubarItem = React.forwardRef<
|
|
|
144
145
|
if (href) {
|
|
145
146
|
return (
|
|
146
147
|
<MenubarPrimitive.Item asChild ref={ref} {...props}>
|
|
147
|
-
<
|
|
148
|
+
<Link href={href} className={classes}>
|
|
148
149
|
{children}
|
|
149
|
-
</
|
|
150
|
+
</Link>
|
|
150
151
|
</MenubarPrimitive.Item>
|
|
151
152
|
)
|
|
152
153
|
}
|
|
@@ -6,6 +6,7 @@ import * as React from 'react';
|
|
|
6
6
|
|
|
7
7
|
import { cn } from '../../../lib';
|
|
8
8
|
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
|
|
9
|
+
import { Link } from '../link';
|
|
9
10
|
|
|
10
11
|
const NavigationMenu = React.forwardRef<
|
|
11
12
|
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
|
@@ -88,7 +89,7 @@ const NavigationMenuLink = React.forwardRef<
|
|
|
88
89
|
if (href) {
|
|
89
90
|
return (
|
|
90
91
|
<NavigationMenuPrimitive.Link asChild ref={ref} {...props}>
|
|
91
|
-
<
|
|
92
|
+
<Link href={href}>{children}</Link>
|
|
92
93
|
</NavigationMenuPrimitive.Link>
|
|
93
94
|
)
|
|
94
95
|
}
|
|
@@ -6,7 +6,7 @@ import * as React from 'react';
|
|
|
6
6
|
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
7
7
|
|
|
8
8
|
import { useIsMobile } from '../../../hooks';
|
|
9
|
-
import { useStoredValue, type StorageType } from '../../../hooks
|
|
9
|
+
import { useStoredValue, type StorageType } from '../../../hooks';
|
|
10
10
|
import { useAppT } from '@djangocfg/i18n';
|
|
11
11
|
import { cn } from '../../../lib/utils';
|
|
12
12
|
import { Button } from '../../forms/button';
|