@djangocfg/ui-nextjs 2.1.251 → 2.1.254

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
@@ -26,7 +26,7 @@ Peer dependencies: `next`, `next-intl`, `react`, `tailwindcss`.
26
26
 
27
27
  ```
28
28
  @djangocfg/ui-nextjs
29
- ├── Re-exports everything from @djangocfg/ui-core (60 components, 13 hooks)
29
+ ├── Re-exports everything from @djangocfg/ui-core (components + hooks)
30
30
  ├── + Next.js locale-aware components (11)
31
31
  ├── + Navigation hooks (useNavigation, useRouter, usePathname)
32
32
  ├── + Browser storage hooks (2)
@@ -49,7 +49,7 @@ All components from `@djangocfg/ui-core` are re-exported.
49
49
  |-----------|-------------|
50
50
  | `NextLink` | Locale-aware link wrapper |
51
51
  | `NextButtonLink` | Button-styled locale-aware link with variants |
52
- | `Sidebar` | Locale-aware navigation sidebar |
52
+ | `Sidebar` | Locale-aware collapsible sidebar; `SidebarTrigger` shows an OS-aware tooltip (`⌘+B` / `Ctrl+B`); `SidebarMenuButton` accepts `tooltip` for icon-rail hints |
53
53
  | `Breadcrumb` | Locale-aware breadcrumbs |
54
54
  | `BreadcrumbNavigation` | High-level breadcrumb component |
55
55
  | `NavigationMenu` | Locale-aware navigation menu |
@@ -91,12 +91,15 @@ function MyComponent() {
91
91
  }
92
92
  ```
93
93
 
94
- ### From ui-core (13)
94
+ ### From ui-core (re-exported)
95
+
96
+ All hooks from `@djangocfg/ui-core/hooks` are available. Common examples:
95
97
 
96
98
  | Hook | Description |
97
99
  |------|-------------|
98
100
  | `useMediaQuery` | Responsive breakpoints |
99
101
  | `useIsMobile` | Mobile detection |
102
+ | `useShortcutModLabel` | `⌘` vs `Ctrl` labels for shortcut tooltips |
100
103
  | `useCopy` | Copy to clipboard |
101
104
  | `useCountdown` | Countdown timer |
102
105
  | `useDebounce` | Debounce values |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-nextjs",
3
- "version": "2.1.251",
3
+ "version": "2.1.254",
4
4
  "description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -85,11 +85,11 @@
85
85
  "check": "tsc --noEmit"
86
86
  },
87
87
  "peerDependencies": {
88
- "@djangocfg/api": "^2.1.251",
89
- "@djangocfg/i18n": "^2.1.251",
90
- "@djangocfg/nextjs": "^2.1.251",
91
- "@djangocfg/ui-core": "^2.1.251",
92
- "@djangocfg/ui-tools": "^2.1.251",
88
+ "@djangocfg/api": "^2.1.254",
89
+ "@djangocfg/i18n": "^2.1.254",
90
+ "@djangocfg/nextjs": "^2.1.254",
91
+ "@djangocfg/ui-core": "^2.1.254",
92
+ "@djangocfg/ui-tools": "^2.1.254",
93
93
  "@types/react": "^19.1.0",
94
94
  "@types/react-dom": "^19.1.0",
95
95
  "consola": "^3.4.2",
@@ -112,7 +112,7 @@
112
112
  "react-chartjs-2": "^5.3.0"
113
113
  },
114
114
  "devDependencies": {
115
- "@djangocfg/typescript-config": "^2.1.251",
115
+ "@djangocfg/typescript-config": "^2.1.254",
116
116
  "@radix-ui/react-dropdown-menu": "^2.1.16",
117
117
  "@radix-ui/react-slot": "^1.2.4",
118
118
  "@types/node": "^24.7.2",
@@ -6,17 +6,27 @@ import { Link } from '../lib/navigation';
6
6
  import * as React from 'react';
7
7
 
8
8
  import {
9
- Button, Input, Separator, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle,
10
- Skeleton, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger
9
+ Button,
10
+ Drawer,
11
+ DrawerContent,
12
+ DrawerDescription,
13
+ DrawerHeader,
14
+ DrawerTitle,
15
+ Input,
16
+ Separator,
17
+ Skeleton,
18
+ Tooltip,
19
+ TooltipContent,
20
+ TooltipProvider,
21
+ TooltipTrigger,
11
22
  } from '@djangocfg/ui-core/components';
12
- import { useIsMobile } from '@djangocfg/ui-core/hooks';
23
+ import { useIsMobile, useShortcutModLabel } from '@djangocfg/ui-core/hooks';
13
24
  import { cn } from '@djangocfg/ui-core/lib';
14
25
  import { Slot } from '@radix-ui/react-slot';
15
26
 
16
27
  const SIDEBAR_COOKIE_NAME = "sidebar_state"
17
28
  const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
18
29
  const SIDEBAR_WIDTH = "16rem"
19
- const SIDEBAR_WIDTH_MOBILE = "min(80vw, 320px)"
20
30
  const SIDEBAR_WIDTH_ICON = "3rem"
21
31
  const SIDEBAR_KEYBOARD_SHORTCUT = "b"
22
32
 
@@ -187,27 +197,37 @@ const Sidebar = React.forwardRef<
187
197
  }
188
198
 
189
199
  if (isMobile) {
200
+ const drawerSide = side === "right" ? "right" : "left"
190
201
  return (
191
- <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
192
- <SheetContent
193
- data-sidebar="sidebar"
194
- data-mobile="true"
195
- className="!w-[--sidebar-width] p-0 text-sidebar-foreground [&>button]:hidden"
196
- style={
197
- {
198
- "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
199
- backgroundColor: "hsl(var(--sidebar-background))",
200
- } as React.CSSProperties
201
- }
202
- side={side}
202
+ <div ref={ref} className={cn("contents", className)} {...props}>
203
+ <Drawer
204
+ open={openMobile}
205
+ onOpenChange={setOpenMobile}
206
+ direction={drawerSide}
207
+ shouldScaleBackground={false}
203
208
  >
204
- <SheetHeader className="sr-only">
205
- <SheetTitle>Sidebar</SheetTitle>
206
- <SheetDescription>Displays the mobile sidebar.</SheetDescription>
207
- </SheetHeader>
208
- <div className="flex h-full w-full flex-col" style={{ backgroundColor: "hsl(var(--sidebar-background))" }}>{children}</div>
209
- </SheetContent>
210
- </Sheet>
209
+ <DrawerContent
210
+ direction={drawerSide}
211
+ data-sidebar="sidebar"
212
+ data-mobile="true"
213
+ className={cn(
214
+ "flex h-[100dvh] max-h-[100dvh] min-h-0 !w-[min(80vw,320px)] max-w-[min(80vw,320px)] flex-col gap-0 border-sidebar-border/60 p-0 text-sidebar-foreground shadow-xl",
215
+ drawerSide === "left" ? "border-r" : "border-l",
216
+ )}
217
+ style={
218
+ {
219
+ backgroundColor: "hsl(var(--sidebar-background))",
220
+ } as React.CSSProperties
221
+ }
222
+ >
223
+ <DrawerHeader className="sr-only">
224
+ <DrawerTitle>Sidebar</DrawerTitle>
225
+ <DrawerDescription>Mobile navigation sidebar.</DrawerDescription>
226
+ </DrawerHeader>
227
+ <div className="flex min-h-0 flex-1 flex-col overflow-hidden">{children}</div>
228
+ </DrawerContent>
229
+ </Drawer>
230
+ </div>
211
231
  )
212
232
  }
213
233
 
@@ -266,10 +286,7 @@ const Sidebar = React.forwardRef<
266
286
  // Adjust the padding for floating and inset variants.
267
287
  variant === "floating" || variant === "inset"
268
288
  ? "p-2"
269
- : cn(
270
- side === "left" && "border-r",
271
- side === "right" && "border-l"
272
- ),
289
+ : undefined,
273
290
  className
274
291
  )}
275
292
  style={{
@@ -280,11 +297,24 @@ const Sidebar = React.forwardRef<
280
297
  <div
281
298
  data-sidebar="sidebar"
282
299
  className={cn(
283
- "flex h-full w-full flex-col bg-sidebar",
284
- variant === "floating" && "rounded-lg border border-sidebar-border shadow"
300
+ "relative flex h-full w-full flex-col overflow-hidden text-sidebar-foreground",
301
+ variant === "floating"
302
+ ? "rounded-lg border border-sidebar-border bg-sidebar shadow"
303
+ : "bg-sidebar",
285
304
  )}
286
305
  >
287
- {children}
306
+ {/* Full-height vertical accent on the seam (gradient runs top → bottom, not along X) */}
307
+ {variant === "sidebar" && (
308
+ <div
309
+ aria-hidden
310
+ className={cn(
311
+ "pointer-events-none absolute inset-y-0 right-0 z-[1] w-px",
312
+ "bg-[linear-gradient(180deg,hsl(var(--sidebar-border)_/_0.02)_0%,hsl(var(--sidebar-border)_/_0.22)_18%,hsl(var(--sidebar-border)_/_0.4)_50%,hsl(var(--sidebar-border)_/_0.2)_82%,hsl(var(--sidebar-border)_/_0.03)_100%)]",
313
+ "dark:bg-[linear-gradient(180deg,hsl(var(--sidebar-border)_/_0.08)_0%,hsl(var(--sidebar-border)_/_0.34)_22%,hsl(var(--sidebar-border)_/_0.55)_50%,hsl(var(--sidebar-border)_/_0.28)_78%,hsl(var(--sidebar-border)_/_0.06)_100%)]"
314
+ )}
315
+ />
316
+ )}
317
+ <div className="relative z-0 flex min-h-0 flex-1 flex-col">{children}</div>
288
318
  </div>
289
319
  </div>
290
320
  </div>
@@ -298,23 +328,33 @@ const SidebarTrigger = React.forwardRef<
298
328
  React.ComponentProps<typeof Button>
299
329
  >(({ className, onClick, ...props }, ref) => {
300
330
  const { toggleSidebar } = useSidebar()
331
+ const mod = useShortcutModLabel()
332
+ const isMobile = useIsMobile()
333
+ const hint = isMobile ? "Toggle sidebar" : `Toggle sidebar (${mod}+B)`
301
334
 
302
335
  return (
303
- <Button
304
- ref={ref}
305
- data-sidebar="trigger"
306
- variant="ghost"
307
- size="icon"
308
- className={cn("h-7 w-7", className)}
309
- onClick={(event) => {
310
- onClick?.(event)
311
- toggleSidebar()
312
- }}
313
- {...props}
314
- >
315
- <PanelLeft />
316
- <span className="sr-only">Toggle Sidebar</span>
317
- </Button>
336
+ <Tooltip delayDuration={400}>
337
+ <TooltipTrigger asChild>
338
+ <Button
339
+ ref={ref}
340
+ data-sidebar="trigger"
341
+ variant="ghost"
342
+ size="icon"
343
+ className={cn("h-7 w-7", className)}
344
+ onClick={(event) => {
345
+ onClick?.(event)
346
+ toggleSidebar()
347
+ }}
348
+ {...props}
349
+ >
350
+ <PanelLeft />
351
+ <span className="sr-only">{hint}</span>
352
+ </Button>
353
+ </TooltipTrigger>
354
+ <TooltipContent side="bottom" align="center" sideOffset={6} className="max-w-[16rem]">
355
+ {hint}
356
+ </TooltipContent>
357
+ </Tooltip>
318
358
  )
319
359
  })
320
360
  SidebarTrigger.displayName = "SidebarTrigger"
@@ -332,7 +372,6 @@ const SidebarRail = React.forwardRef<
332
372
  aria-label="Toggle Sidebar"
333
373
  tabIndex={-1}
334
374
  onClick={toggleSidebar}
335
- title="Toggle Sidebar"
336
375
  className={cn(
337
376
  "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
338
377
  "group-data-[side=left]:cursor-w-resize group-data-[side=right]:cursor-e-resize",
@@ -494,7 +533,8 @@ const SidebarGroupLabel = React.forwardRef<
494
533
  data-sidebar="group-label"
495
534
  className={cn(
496
535
  "flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
497
- "group-data-[state=collapsed]:group-data-[collapsible=icon]:-mt-8 group-data-[state=collapsed]:group-data-[collapsible=icon]:opacity-0",
536
+ // Collapsed rail: label is visually hidden but was still hit-testing; -mt-8 overlaps the last nav row.
537
+ "group-data-[state=collapsed]:group-data-[collapsible=icon]:pointer-events-none group-data-[state=collapsed]:group-data-[collapsible=icon]:-mt-8 group-data-[state=collapsed]:group-data-[collapsible=icon]:opacity-0",
498
538
  className
499
539
  )}
500
540
  {...props}
@@ -649,11 +689,8 @@ const SidebarMenuButton = React.forwardRef<
649
689
  return buttonContent
650
690
  }
651
691
 
652
- if (typeof tooltip === "string") {
653
- tooltip = {
654
- children: tooltip,
655
- }
656
- }
692
+ const tooltipContentProps: React.ComponentProps<typeof TooltipContent> =
693
+ typeof tooltip === "string" ? { children: tooltip } : { ...tooltip }
657
694
 
658
695
  return (
659
696
  <Tooltip>
@@ -661,8 +698,10 @@ const SidebarMenuButton = React.forwardRef<
661
698
  <TooltipContent
662
699
  side="right"
663
700
  align="center"
701
+ sideOffset={8}
702
+ avoidCollisions={false}
664
703
  hidden={state !== "collapsed" || isMobile}
665
- {...tooltip}
704
+ {...tooltipContentProps}
666
705
  />
667
706
  </Tooltip>
668
707
  )
@@ -709,11 +748,11 @@ const SidebarMenuBadge = React.forwardRef<
709
748
  ref={ref}
710
749
  data-sidebar="menu-badge"
711
750
  className={cn(
712
- "pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
713
- "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
751
+ "pointer-events-none absolute right-1 flex h-4 min-w-4 select-none items-center justify-center rounded-md px-1 text-[10px] font-medium tabular-nums leading-none text-muted-foreground/75",
752
+ "peer-hover/menu-button:text-muted-foreground peer-data-[active=true]/menu-button:text-foreground/65",
714
753
  "peer-data-[size=sm]/menu-button:top-1",
715
754
  "peer-data-[size=default]/menu-button:top-1.5",
716
- "peer-data-[size=lg]/menu-button:top-2.5",
755
+ "peer-data-[size=lg]/menu-button:top-2",
717
756
  "group-data-[state=collapsed]:group-data-[collapsible=icon]:hidden",
718
757
  className
719
758
  )}
@@ -71,7 +71,6 @@ export function ThemeToggle({ className, size = 'auto' }: ThemeToggleProps) {
71
71
  size="icon"
72
72
  onClick={toggleTheme}
73
73
  className={cn(buttonSize, className)}
74
- title={`Switch to ${resolvedTheme === 'light' ? 'dark' : 'light'} theme`}
75
74
  >
76
75
  {resolvedTheme === 'light' ? (
77
76
  <Sun className={iconSize} />