@djangocfg/ui-core 2.1.294 → 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 +83 -29
- package/package.json +4 -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 +15 -26
- package/src/hooks/media/index.ts +5 -0
- package/src/hooks/router/README.md +7 -5
- package/src/hooks/router/adapters/nextjs.tsx +41 -1
- 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
|
|
96
|
+
|
|
97
|
+
| Hook | Description |
|
|
98
|
+
|------|-------------|
|
|
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
|
|
87
115
|
|
|
88
116
|
| Hook | Description |
|
|
89
117
|
|------|-------------|
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
|
98
|
-
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
|
106
|
-
|
|
107
|
-
| `
|
|
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,6 +150,10 @@ 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
|
|
119
157
|
```
|
|
120
158
|
|
|
121
159
|
## Router Hooks
|
|
@@ -153,17 +191,19 @@ setPage((p) => p + 1); // ?page=2 — `page=1` is dropped (clearOnDefault)
|
|
|
153
191
|
|
|
154
192
|
### Next.js adapter
|
|
155
193
|
|
|
156
|
-
In Next apps, mount
|
|
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`:
|
|
157
195
|
|
|
158
196
|
```tsx
|
|
159
|
-
import { NextRouterAdapter } from '@djangocfg/ui-core/adapters/nextjs';
|
|
197
|
+
import { NextRouterAdapter, NextLinkProvider } from '@djangocfg/ui-core/adapters/nextjs';
|
|
160
198
|
|
|
161
199
|
<NextRouterAdapter>
|
|
162
|
-
<
|
|
200
|
+
<NextLinkProvider>
|
|
201
|
+
<App />
|
|
202
|
+
</NextLinkProvider>
|
|
163
203
|
</NextRouterAdapter>
|
|
164
204
|
```
|
|
165
205
|
|
|
166
|
-
`next` is an **optional peer dependency** — Wails / Electron / Vite consumers don't pull it in.
|
|
206
|
+
`next` is an **optional peer dependency** — Wails / Electron / Vite consumers don't pull it in. `@djangocfg/layouts/BaseApp` mounts both adapters automatically.
|
|
167
207
|
|
|
168
208
|
## Theme Palette Hooks
|
|
169
209
|
|
|
@@ -338,7 +378,7 @@ import '@djangocfg/ui-core/styles/globals';
|
|
|
338
378
|
| `@djangocfg/ui-core` | All components & hooks |
|
|
339
379
|
| `@djangocfg/ui-core/components` | Components only |
|
|
340
380
|
| `@djangocfg/ui-core/hooks` | Hooks only (incl. router hooks) |
|
|
341
|
-
| `@djangocfg/ui-core/adapters/nextjs` | `<NextRouterAdapter>` for Next.js apps (optional peer: `next`) |
|
|
381
|
+
| `@djangocfg/ui-core/adapters/nextjs` | `<NextRouterAdapter>` + `<NextLinkProvider>` for Next.js apps (optional peer: `next`) |
|
|
342
382
|
| `@djangocfg/ui-core/lib` | Utilities (cn, etc.) |
|
|
343
383
|
| `@djangocfg/ui-core/lib/dialog-service` | Dialog service |
|
|
344
384
|
| `@djangocfg/ui-core/utils` | Runtime utilities (emitRuntimeError) |
|
|
@@ -359,15 +399,29 @@ try {
|
|
|
359
399
|
}
|
|
360
400
|
```
|
|
361
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
|
+
|
|
362
418
|
## What's NOT included (use ui-nextjs)
|
|
363
419
|
|
|
364
420
|
These features require Next.js or browser storage APIs:
|
|
365
421
|
|
|
366
|
-
- `Sidebar` —
|
|
367
|
-
- `Breadcrumb`, `BreadcrumbNavigation` —
|
|
368
|
-
- `
|
|
369
|
-
- `Pagination`, `SSRPagination` — uses next/link
|
|
370
|
-
- `DropdownMenu` — uses next/link
|
|
422
|
+
- `Sidebar` — `'use client'` heavy, lives in ui-nextjs
|
|
423
|
+
- `Breadcrumb`, `BreadcrumbNavigation` — same
|
|
424
|
+
- `Pagination`, `SSRPagination` — same
|
|
371
425
|
- `DownloadButton` — uses localStorage
|
|
372
426
|
- `useTheme` — uses next-themes
|
|
373
427
|
|
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",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"playground": "playground dev"
|
|
92
92
|
},
|
|
93
93
|
"peerDependencies": {
|
|
94
|
-
"@djangocfg/i18n": "^2.1.
|
|
94
|
+
"@djangocfg/i18n": "^2.1.297",
|
|
95
95
|
"consola": "^3.4.2",
|
|
96
96
|
"lucide-react": "^0.545.0",
|
|
97
97
|
"moment": "^2.30.1",
|
|
@@ -159,9 +159,9 @@
|
|
|
159
159
|
"vaul": "1.1.2"
|
|
160
160
|
},
|
|
161
161
|
"devDependencies": {
|
|
162
|
-
"@djangocfg/i18n": "^2.1.
|
|
162
|
+
"@djangocfg/i18n": "^2.1.297",
|
|
163
163
|
"@djangocfg/playground": "workspace:*",
|
|
164
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
164
|
+
"@djangocfg/typescript-config": "^2.1.297",
|
|
165
165
|
"@types/node": "^24.7.2",
|
|
166
166
|
"@types/react": "^19.1.0",
|
|
167
167
|
"@types/react-dom": "^19.1.0",
|
|
@@ -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';
|
|
@@ -4,7 +4,7 @@ import { Check, ChevronsUpDown } from 'lucide-react';
|
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
|
|
6
6
|
import { useAppT } from '@djangocfg/i18n';
|
|
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
|
import { Badge } from '../data/badge';
|
|
10
10
|
import { Button } from '../forms/button';
|
|
@@ -4,7 +4,7 @@ import { Check, ChevronsUpDown, X } from 'lucide-react';
|
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
|
|
6
6
|
import { useAppT } from '@djangocfg/i18n';
|
|
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
|
import { Badge } from '../data/badge';
|
|
10
10
|
import { Button } from '../forms/button';
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { Car, ImageIcon, MapPin, Package, User } from 'lucide-react';
|
|
8
8
|
import React, { forwardRef } from 'react';
|
|
9
9
|
|
|
10
|
-
import { useImageLoader } from '../../../hooks
|
|
10
|
+
import { useImageLoader } from '../../../hooks';
|
|
11
11
|
import { cn } from '../../../lib/utils';
|
|
12
12
|
|
|
13
13
|
export interface ImageWithFallbackProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'onLoad' | 'onError'> {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export { useBrowserDetect } from './useBrowserDetect';
|
|
4
|
+
export type { BrowserInfo } from './useBrowserDetect';
|
|
5
|
+
export { useDeviceDetect } from './useDeviceDetect';
|
|
6
|
+
export type { DeviceDetectResult } from './useDeviceDetect';
|
|
7
|
+
export { useShortcutModLabel } from './useShortcutModLabel';
|