@djangocfg/ui-core 2.1.285 → 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 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 (8)
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.285",
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.285",
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.285",
151
+ "@djangocfg/i18n": "^2.1.286",
152
152
  "@djangocfg/playground": "workspace:*",
153
- "@djangocfg/typescript-config": "^2.1.285",
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",
@@ -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
+ };