@djangocfg/ui-core 2.1.284 → 2.1.286
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 +17 -2
- package/package.json +4 -4
- package/src/components/index.ts +2 -0
- package/src/components/overlay/side-panel/index.tsx +255 -0
package/README.md
CHANGED
|
@@ -51,11 +51,26 @@ import { Button, Dialog, Table } from '@djangocfg/ui-core';
|
|
|
51
51
|
### Layout (8)
|
|
52
52
|
`Card` `Separator` `Skeleton` `AspectRatio` `Sticky` `ScrollArea` `Resizable` `Section`
|
|
53
53
|
|
|
54
|
-
### Overlay (
|
|
55
|
-
`Dialog` `AlertDialog` `Sheet` `Drawer` `Popover` `HoverCard` `Tooltip` `ResponsiveSheet`
|
|
54
|
+
### Overlay (9)
|
|
55
|
+
`Dialog` `AlertDialog` `Sheet` `Drawer` `Popover` `HoverCard` `Tooltip` `ResponsiveSheet` `SidePanel`
|
|
56
56
|
|
|
57
57
|
Default **`TooltipContent`** styling uses semantic **popover** tokens (`bg-popover`, `text-popover-foreground`, `border-border`, shadow) — not `primary` — so hints read as neutral floating UI.
|
|
58
58
|
|
|
59
|
+
**`SidePanel`** — non-modal side drawer for inspector panels, playgrounds, filters. Slides in from the right (`side="right"`, default) or left edge. Unlike `Sheet`/`Drawer`, it does NOT lock the rest of the page — surrounding UI stays clickable and focusable. Esc-to-close (toggleable) and swipe-to-close via vaul. Use when you want a slide-in surface that co-exists with the page below; for modal side surfaces, prefer `Sheet`.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<SidePanel open={open} onOpenChange={setOpen} side="right">
|
|
63
|
+
<SidePanel.Content width="440px">
|
|
64
|
+
<SidePanel.Header>
|
|
65
|
+
<SidePanel.Title>Details</SidePanel.Title>
|
|
66
|
+
<SidePanel.Close className="ml-auto" />
|
|
67
|
+
</SidePanel.Header>
|
|
68
|
+
<SidePanel.Body>…</SidePanel.Body>
|
|
69
|
+
<SidePanel.Footer>…</SidePanel.Footer>
|
|
70
|
+
</SidePanel.Content>
|
|
71
|
+
</SidePanel>
|
|
72
|
+
```
|
|
73
|
+
|
|
59
74
|
### Navigation (8)
|
|
60
75
|
`Tabs` `Accordion` `Collapsible` `Command` `ContextMenu` `DropdownMenu` `Menubar` `NavigationMenu`
|
|
61
76
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-core",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.286",
|
|
4
4
|
"description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"playground": "playground dev"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
|
-
"@djangocfg/i18n": "^2.1.
|
|
89
|
+
"@djangocfg/i18n": "^2.1.286",
|
|
90
90
|
"consola": "^3.4.2",
|
|
91
91
|
"lucide-react": "^0.545.0",
|
|
92
92
|
"moment": "^2.30.1",
|
|
@@ -148,9 +148,9 @@
|
|
|
148
148
|
"vaul": "1.1.2"
|
|
149
149
|
},
|
|
150
150
|
"devDependencies": {
|
|
151
|
-
"@djangocfg/i18n": "^2.1.
|
|
151
|
+
"@djangocfg/i18n": "^2.1.286",
|
|
152
152
|
"@djangocfg/playground": "workspace:*",
|
|
153
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
153
|
+
"@djangocfg/typescript-config": "^2.1.286",
|
|
154
154
|
"@types/node": "^24.7.2",
|
|
155
155
|
"@types/react": "^19.1.0",
|
|
156
156
|
"@types/react-dom": "^19.1.0",
|
package/src/components/index.ts
CHANGED
|
@@ -43,6 +43,8 @@ export { Popover, PopoverContent, PopoverTrigger, PopoverAnchor, PopoverArrow }
|
|
|
43
43
|
export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription, SheetPortal, SheetOverlay } from './overlay/sheet';
|
|
44
44
|
export { Drawer, DrawerTrigger, DrawerClose, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription, DrawerPortal, DrawerOverlay } from './overlay/drawer';
|
|
45
45
|
export { ResponsiveSheet, ResponsiveSheetContent, ResponsiveSheetHeader, ResponsiveSheetTitle, ResponsiveSheetDescription, ResponsiveSheetFooter } from './overlay/responsive-sheet';
|
|
46
|
+
export { SidePanel, SidePanelContent, SidePanelHeader, SidePanelTitle, SidePanelDescription, SidePanelBody, SidePanelFooter, SidePanelClose } from './overlay/side-panel';
|
|
47
|
+
export type { SidePanelProps, SidePanelContentProps, SidePanelCloseProps } from './overlay/side-panel';
|
|
46
48
|
export { HoverCard, HoverCardContent, HoverCardTrigger } from './overlay/hover-card';
|
|
47
49
|
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, SafeTooltipProvider } from './overlay/tooltip';
|
|
48
50
|
export type { SafeTooltipProviderProps } from './overlay/tooltip';
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SidePanel — a non-modal side drawer.
|
|
5
|
+
*
|
|
6
|
+
* Use for inspector panels, playgrounds, filters — anywhere you want a
|
|
7
|
+
* slide-in surface that does NOT lock out the rest of the page. The
|
|
8
|
+
* surrounding UI stays clickable and focusable; only Esc (optional) and
|
|
9
|
+
* the close button dismiss the panel.
|
|
10
|
+
*
|
|
11
|
+
* Differences from the existing ``Drawer``:
|
|
12
|
+
* - ``modal={false}`` — no focus trap, no scroll-lock on <body>.
|
|
13
|
+
* - No backdrop overlay.
|
|
14
|
+
* - No ``shouldScaleBackground`` (vaul's iOS-style fancy scale looks
|
|
15
|
+
* wrong for side panels — reserved for bottom sheets).
|
|
16
|
+
* - Opinionated for ``right``/``left`` directions; vaul still drives
|
|
17
|
+
* the transform + swipe-to-close gesture underneath.
|
|
18
|
+
*
|
|
19
|
+
* Layout is intentionally similar to our Sheet/Drawer components so
|
|
20
|
+
* callers feel at home.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as React from 'react';
|
|
24
|
+
import { Drawer as DrawerPrimitive } from 'vaul';
|
|
25
|
+
import { X } from 'lucide-react';
|
|
26
|
+
|
|
27
|
+
import { cn } from '../../../lib/utils';
|
|
28
|
+
|
|
29
|
+
// ─── Root ─────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export interface SidePanelProps {
|
|
32
|
+
open: boolean;
|
|
33
|
+
onOpenChange: (open: boolean) => void;
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
/** ``'right'`` (default) slides in from the right edge; ``'left'`` from the left. */
|
|
36
|
+
side?: 'right' | 'left';
|
|
37
|
+
/** Close when the user presses Escape. Default ``true``. Disable when
|
|
38
|
+
* the parent wants custom handling or Esc is bound to something else. */
|
|
39
|
+
closeOnEsc?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SidePanel: React.FC<SidePanelProps> & {
|
|
43
|
+
Content: typeof SidePanelContent;
|
|
44
|
+
Header: typeof SidePanelHeader;
|
|
45
|
+
Title: typeof SidePanelTitle;
|
|
46
|
+
Description: typeof SidePanelDescription;
|
|
47
|
+
Body: typeof SidePanelBody;
|
|
48
|
+
Footer: typeof SidePanelFooter;
|
|
49
|
+
Close: typeof SidePanelClose;
|
|
50
|
+
} = ({ open, onOpenChange, children, side = 'right', closeOnEsc = true }) => {
|
|
51
|
+
// Esc handling: vaul's built-in closes on Esc only when modal=true.
|
|
52
|
+
// We're non-modal, so we install our own listener — gated on ``open``
|
|
53
|
+
// to avoid swallowing Esc globally while the panel is closed.
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
if (!open || !closeOnEsc) return;
|
|
56
|
+
const handler = (e: KeyboardEvent) => {
|
|
57
|
+
if (e.key === 'Escape') onOpenChange(false);
|
|
58
|
+
};
|
|
59
|
+
window.addEventListener('keydown', handler);
|
|
60
|
+
return () => window.removeEventListener('keydown', handler);
|
|
61
|
+
}, [open, closeOnEsc, onOpenChange]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<DrawerPrimitive.Root
|
|
65
|
+
open={open}
|
|
66
|
+
onOpenChange={onOpenChange}
|
|
67
|
+
direction={side}
|
|
68
|
+
modal={false}
|
|
69
|
+
shouldScaleBackground={false}
|
|
70
|
+
// vaul defaults to mutating ``<body>`` styles (position:fixed,
|
|
71
|
+
// overflow:hidden, pointer-events:none) to support swipe-to-
|
|
72
|
+
// close. For a non-modal side panel that behaviour is wrong —
|
|
73
|
+
// it disables every interaction outside the panel including
|
|
74
|
+
// page scroll. ``noBodyStyles`` tells vaul to keep its hands
|
|
75
|
+
// off the document.
|
|
76
|
+
noBodyStyles
|
|
77
|
+
disablePreventScroll
|
|
78
|
+
dismissible
|
|
79
|
+
>
|
|
80
|
+
<SidePanelSideContext.Provider value={side}>
|
|
81
|
+
{children}
|
|
82
|
+
</SidePanelSideContext.Provider>
|
|
83
|
+
</DrawerPrimitive.Root>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
SidePanel.displayName = 'SidePanel';
|
|
87
|
+
|
|
88
|
+
// Carries the side down to Content so it can place border + transform correctly.
|
|
89
|
+
const SidePanelSideContext = React.createContext<'right' | 'left'>('right');
|
|
90
|
+
|
|
91
|
+
// ─── Content ──────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
export interface SidePanelContentProps extends React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> {
|
|
94
|
+
/** CSS width (any valid value: ``'440px'``, ``'32rem'``, ``'min(440px, 90vw)'``).
|
|
95
|
+
* Default ``'440px'``. Override via ``className`` with ``w-*`` if you
|
|
96
|
+
* prefer Tailwind. */
|
|
97
|
+
width?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const SidePanelContent = React.forwardRef<
|
|
101
|
+
React.ElementRef<typeof DrawerPrimitive.Content>,
|
|
102
|
+
SidePanelContentProps
|
|
103
|
+
>(({ className, children, width = '440px', style, ...props }, ref) => {
|
|
104
|
+
const side = React.useContext(SidePanelSideContext);
|
|
105
|
+
const positioning =
|
|
106
|
+
side === 'right' ? 'inset-y-0 right-0 border-l' : 'inset-y-0 left-0 border-r';
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<DrawerPrimitive.Portal>
|
|
110
|
+
<DrawerPrimitive.Content
|
|
111
|
+
ref={ref}
|
|
112
|
+
className={cn(
|
|
113
|
+
'fixed z-500 flex flex-col bg-background shadow-2xl shadow-black/20',
|
|
114
|
+
'h-full max-w-[95vw]',
|
|
115
|
+
positioning,
|
|
116
|
+
className,
|
|
117
|
+
)}
|
|
118
|
+
style={{
|
|
119
|
+
width,
|
|
120
|
+
// Animate both the slide-in (transform, driven by vaul)
|
|
121
|
+
// and width changes (when callers swap width to reveal
|
|
122
|
+
// secondary content). Separate transition strings so
|
|
123
|
+
// the two can have different curves if needed.
|
|
124
|
+
transition:
|
|
125
|
+
'transform 300ms cubic-bezier(0.32, 0.72, 0, 1),' +
|
|
126
|
+
' width 250ms cubic-bezier(0.32, 0.72, 0, 1)',
|
|
127
|
+
...style,
|
|
128
|
+
}}
|
|
129
|
+
{...props}
|
|
130
|
+
>
|
|
131
|
+
{children}
|
|
132
|
+
</DrawerPrimitive.Content>
|
|
133
|
+
</DrawerPrimitive.Portal>
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
SidePanelContent.displayName = 'SidePanelContent';
|
|
137
|
+
|
|
138
|
+
// ─── Header / Title / Description ─────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
const SidePanelHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
141
|
+
({ className, ...props }, ref) => (
|
|
142
|
+
<div
|
|
143
|
+
ref={ref}
|
|
144
|
+
className={cn(
|
|
145
|
+
'shrink-0 flex items-center gap-3 px-4 h-12 border-b bg-muted/20',
|
|
146
|
+
className,
|
|
147
|
+
)}
|
|
148
|
+
{...props}
|
|
149
|
+
/>
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
SidePanelHeader.displayName = 'SidePanelHeader';
|
|
153
|
+
|
|
154
|
+
const SidePanelTitle = React.forwardRef<
|
|
155
|
+
React.ElementRef<typeof DrawerPrimitive.Title>,
|
|
156
|
+
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
|
157
|
+
>(({ className, ...props }, ref) => (
|
|
158
|
+
<DrawerPrimitive.Title
|
|
159
|
+
ref={ref}
|
|
160
|
+
className={cn(
|
|
161
|
+
'text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/60',
|
|
162
|
+
className,
|
|
163
|
+
)}
|
|
164
|
+
{...props}
|
|
165
|
+
/>
|
|
166
|
+
));
|
|
167
|
+
SidePanelTitle.displayName = 'SidePanelTitle';
|
|
168
|
+
|
|
169
|
+
const SidePanelDescription = React.forwardRef<
|
|
170
|
+
React.ElementRef<typeof DrawerPrimitive.Description>,
|
|
171
|
+
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
|
172
|
+
>(({ className, ...props }, ref) => (
|
|
173
|
+
<DrawerPrimitive.Description
|
|
174
|
+
ref={ref}
|
|
175
|
+
className={cn('text-xs text-muted-foreground', className)}
|
|
176
|
+
{...props}
|
|
177
|
+
/>
|
|
178
|
+
));
|
|
179
|
+
SidePanelDescription.displayName = 'SidePanelDescription';
|
|
180
|
+
|
|
181
|
+
// ─── Body / Footer ────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
const SidePanelBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
184
|
+
({ className, ...props }, ref) => (
|
|
185
|
+
<div
|
|
186
|
+
ref={ref}
|
|
187
|
+
className={cn('flex-1 min-h-0 overflow-y-auto', className)}
|
|
188
|
+
{...props}
|
|
189
|
+
/>
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
SidePanelBody.displayName = 'SidePanelBody';
|
|
193
|
+
|
|
194
|
+
const SidePanelFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
195
|
+
({ className, ...props }, ref) => (
|
|
196
|
+
<div
|
|
197
|
+
ref={ref}
|
|
198
|
+
className={cn('shrink-0 border-t px-4 py-3 bg-background/95', className)}
|
|
199
|
+
{...props}
|
|
200
|
+
/>
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
SidePanelFooter.displayName = 'SidePanelFooter';
|
|
204
|
+
|
|
205
|
+
// ─── Close ────────────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
export interface SidePanelCloseProps
|
|
208
|
+
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'type' | 'children'> {
|
|
209
|
+
/** Accessible label. Default ``"Close panel"``. */
|
|
210
|
+
label?: string;
|
|
211
|
+
/** Replace the default ``X`` icon with custom content. */
|
|
212
|
+
children?: React.ReactNode;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const SidePanelClose = React.forwardRef<HTMLButtonElement, SidePanelCloseProps>(
|
|
216
|
+
({ className, label = 'Close panel', children, ...props }, ref) => (
|
|
217
|
+
<DrawerPrimitive.Close asChild>
|
|
218
|
+
<button
|
|
219
|
+
ref={ref}
|
|
220
|
+
type="button"
|
|
221
|
+
aria-label={label}
|
|
222
|
+
title={`${label} (Esc)`}
|
|
223
|
+
className={cn(
|
|
224
|
+
'shrink-0 h-7 w-7 inline-flex items-center justify-center rounded-md',
|
|
225
|
+
'text-muted-foreground hover:text-foreground hover:bg-muted transition-colors',
|
|
226
|
+
className,
|
|
227
|
+
)}
|
|
228
|
+
{...props}
|
|
229
|
+
>
|
|
230
|
+
{children ?? <X className="h-4 w-4" />}
|
|
231
|
+
</button>
|
|
232
|
+
</DrawerPrimitive.Close>
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
SidePanelClose.displayName = 'SidePanelClose';
|
|
236
|
+
|
|
237
|
+
// Attach subcomponents to the root for ergonomic dot-access.
|
|
238
|
+
SidePanel.Content = SidePanelContent;
|
|
239
|
+
SidePanel.Header = SidePanelHeader;
|
|
240
|
+
SidePanel.Title = SidePanelTitle;
|
|
241
|
+
SidePanel.Description = SidePanelDescription;
|
|
242
|
+
SidePanel.Body = SidePanelBody;
|
|
243
|
+
SidePanel.Footer = SidePanelFooter;
|
|
244
|
+
SidePanel.Close = SidePanelClose;
|
|
245
|
+
|
|
246
|
+
export {
|
|
247
|
+
SidePanel,
|
|
248
|
+
SidePanelContent,
|
|
249
|
+
SidePanelHeader,
|
|
250
|
+
SidePanelTitle,
|
|
251
|
+
SidePanelDescription,
|
|
252
|
+
SidePanelBody,
|
|
253
|
+
SidePanelFooter,
|
|
254
|
+
SidePanelClose,
|
|
255
|
+
};
|