@djangocfg/ui-core 2.1.299 → 2.1.301

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
@@ -71,6 +71,34 @@ Default **`TooltipContent`** styling uses semantic **popover** tokens (`bg-popov
71
71
  </SidePanel>
72
72
  ```
73
73
 
74
+ **`Drawer`** — modal vaul-based panel that slides in from any edge (`top` / `right` / `bottom` / `left`). Picks a size from a preset table or takes an explicit CSS length; both are applied via inline `style` so vaul's first-paint measurement matches the final layout (no inset miscalc).
75
+
76
+ ```tsx
77
+ <Drawer direction="right">
78
+ <DrawerTrigger asChild><Button>Open</Button></DrawerTrigger>
79
+ <DrawerContent direction="right" size="lg">
80
+ <DrawerHeader>
81
+ <DrawerTitle>Details</DrawerTitle>
82
+ </DrawerHeader>
83
+
84
+ </DrawerContent>
85
+ </Drawer>
86
+ ```
87
+
88
+ Size presets — `sm` `md` `lg` `xl` `full`. Maps to width for `left`/`right`, height for `top`/`bottom`:
89
+
90
+ | Size | Horizontal width | Vertical height |
91
+ |------|-------------------|------------------|
92
+ | sm | `min(100vw, 360px)` | `min(100vh, 240px)` |
93
+ | md | `min(100vw, 480px)` | `min(100vh, 360px)` |
94
+ | lg | `min(100vw, 640px)` | `min(100vh, 480px)` |
95
+ | xl | `min(100vw, 800px)` | `min(100vh, 640px)` |
96
+ | full | `100vw` | `100vh` |
97
+
98
+ If you need an exact size, pass `width` / `height` (a CSS length string or number → px). Inline-applied so the vaul measurement is correct on first paint.
99
+
100
+ **Migration note** — the default size changed (was hardcoded `280px` for left/right, `auto` for top/bottom). New default is `size="md"`. Pass `size="sm"` (~360px) for the closest old left/right behavior, or set an explicit `width` / `height`.
101
+
74
102
  ### Navigation (8)
75
103
  `Tabs` `Accordion` `Collapsible` `Command` `ContextMenu` `DropdownMenu` `Menubar` `NavigationMenu`
76
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.299",
3
+ "version": "2.1.301",
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.299",
94
+ "@djangocfg/i18n": "^2.1.301",
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.299",
162
+ "@djangocfg/i18n": "^2.1.301",
163
163
  "@djangocfg/playground": "workspace:*",
164
- "@djangocfg/typescript-config": "^2.1.299",
164
+ "@djangocfg/typescript-config": "^2.1.301",
165
165
  "@types/node": "^24.7.2",
166
166
  "@types/react": "^19.1.0",
167
167
  "@types/react-dom": "^19.1.0",
@@ -1,4 +1,5 @@
1
- import { defineStory } from '@djangocfg/playground';
1
+ import * as React from 'react';
2
+ import { defineStory, useSelect } from '@djangocfg/playground';
2
3
  import {
3
4
  Drawer,
4
5
  DrawerTrigger,
@@ -8,6 +9,7 @@ import {
8
9
  DrawerTitle,
9
10
  DrawerDescription,
10
11
  DrawerClose,
12
+ type DrawerSize,
11
13
  } from '.';
12
14
  import { Button } from '../../forms/button';
13
15
  import { Input } from '../../forms/input';
@@ -129,3 +131,145 @@ export const ActionSheet = () => (
129
131
  </DrawerContent>
130
132
  </Drawer>
131
133
  );
134
+
135
+ const SIZES: readonly DrawerSize[] = ['sm', 'md', 'lg', 'xl', 'full'] as const;
136
+
137
+ export const Sizes = () => {
138
+ const [size, setSize] = React.useState<DrawerSize>('md');
139
+ const [open, setOpen] = React.useState(false);
140
+
141
+ return (
142
+ <div className="space-y-4">
143
+ <div className="flex flex-wrap gap-2">
144
+ {SIZES.map((s) => (
145
+ <Button
146
+ key={s}
147
+ variant={size === s ? 'default' : 'outline'}
148
+ onClick={() => {
149
+ setSize(s);
150
+ setOpen(true);
151
+ }}
152
+ >
153
+ {s}
154
+ </Button>
155
+ ))}
156
+ </div>
157
+ <p className="text-sm text-muted-foreground">
158
+ Current size: <code>{size}</code>. Direction: right.
159
+ </p>
160
+ <Drawer open={open} onOpenChange={setOpen} direction="right">
161
+ <DrawerContent direction="right" size={size}>
162
+ <DrawerHeader>
163
+ <DrawerTitle>Size: {size}</DrawerTitle>
164
+ <DrawerDescription>
165
+ Width preset is applied via inline style — vaul measures it
166
+ correctly on first paint.
167
+ </DrawerDescription>
168
+ </DrawerHeader>
169
+ <div className="p-4 text-sm">Content adapts to the chosen size.</div>
170
+ <DrawerFooter>
171
+ <DrawerClose asChild>
172
+ <Button variant="outline">Close</Button>
173
+ </DrawerClose>
174
+ </DrawerFooter>
175
+ </DrawerContent>
176
+ </Drawer>
177
+ </div>
178
+ );
179
+ };
180
+
181
+ export const Directions = () => {
182
+ const [direction] = useSelect('direction', {
183
+ options: ['top', 'right', 'bottom', 'left'] as const,
184
+ defaultValue: 'right',
185
+ label: 'Direction',
186
+ description: 'Edge from which the drawer slides.',
187
+ });
188
+ const [size] = useSelect('size', {
189
+ options: ['sm', 'md', 'lg', 'xl', 'full'] as const,
190
+ defaultValue: 'md',
191
+ label: 'Size',
192
+ description: 'Width (left/right) or height (top/bottom).',
193
+ });
194
+
195
+ return (
196
+ <Drawer direction={direction}>
197
+ <DrawerTrigger asChild>
198
+ <Button variant="outline">Open from {direction}</Button>
199
+ </DrawerTrigger>
200
+ <DrawerContent direction={direction} size={size}>
201
+ <DrawerHeader>
202
+ <DrawerTitle>Direction: {direction}</DrawerTitle>
203
+ <DrawerDescription>
204
+ Size presets adapt: horizontal directions size width, vertical
205
+ ones size height.
206
+ </DrawerDescription>
207
+ </DrawerHeader>
208
+ <div className="p-4 text-sm">size = {size}</div>
209
+ <DrawerFooter>
210
+ <DrawerClose asChild>
211
+ <Button variant="outline">Close</Button>
212
+ </DrawerClose>
213
+ </DrawerFooter>
214
+ </DrawerContent>
215
+ </Drawer>
216
+ );
217
+ };
218
+
219
+ export const CustomWidth = () => (
220
+ <Drawer direction="right">
221
+ <DrawerTrigger asChild>
222
+ <Button variant="outline">Open with width=720px</Button>
223
+ </DrawerTrigger>
224
+ <DrawerContent direction="right" width="720px">
225
+ <DrawerHeader>
226
+ <DrawerTitle>Custom width</DrawerTitle>
227
+ <DrawerDescription>
228
+ Explicit <code>width</code> overrides the <code>size</code> preset.
229
+ </DrawerDescription>
230
+ </DrawerHeader>
231
+ <div className="p-4 text-sm">Width is exactly 720px.</div>
232
+ <DrawerFooter>
233
+ <DrawerClose asChild>
234
+ <Button variant="outline">Close</Button>
235
+ </DrawerClose>
236
+ </DrawerFooter>
237
+ </DrawerContent>
238
+ </Drawer>
239
+ );
240
+
241
+ export const NarrowViewport = () => (
242
+ <div className="space-y-3">
243
+ <p className="text-sm text-muted-foreground">
244
+ Resize the browser narrower than the size preset (e.g. &lt; 480px for
245
+ <code> md</code>) to see <code>min(100vw, …)</code> clamp the drawer
246
+ to the viewport. This is the regression we now guard against.
247
+ </p>
248
+ <div
249
+ style={{ width: 360 }}
250
+ className="rounded border border-dashed p-4"
251
+ >
252
+ <Drawer direction="right">
253
+ <DrawerTrigger asChild>
254
+ <Button variant="outline">Open in 360px container</Button>
255
+ </DrawerTrigger>
256
+ <DrawerContent direction="right" size="md">
257
+ <DrawerHeader>
258
+ <DrawerTitle>Narrow viewport</DrawerTitle>
259
+ <DrawerDescription>
260
+ Drawer is portaled to the body — viewport width still applies.
261
+ </DrawerDescription>
262
+ </DrawerHeader>
263
+ <div className="p-4 text-sm">
264
+ min(100vw, 480px) keeps the drawer within bounds on tiny screens.
265
+ </div>
266
+ <DrawerFooter>
267
+ <DrawerClose asChild>
268
+ <Button variant="outline">Close</Button>
269
+ </DrawerClose>
270
+ </DrawerFooter>
271
+ </DrawerContent>
272
+ </Drawer>
273
+ </div>
274
+ </div>
275
+ );
@@ -37,19 +37,66 @@ const DrawerOverlay = React.forwardRef<
37
37
  ))
38
38
  DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
39
39
 
40
+ /** Drawer size preset. Maps to width for left/right, height for top/bottom. */
41
+ export type DrawerSize = 'sm' | 'md' | 'lg' | 'xl' | 'full'
42
+
43
+ const horizontalSizePresets: Record<DrawerSize, string> = {
44
+ sm: 'min(100vw, 360px)',
45
+ md: 'min(100vw, 480px)',
46
+ lg: 'min(100vw, 640px)',
47
+ xl: 'min(100vw, 800px)',
48
+ full: '100vw',
49
+ };
50
+
51
+ const verticalSizePresets: Record<DrawerSize, string> = {
52
+ sm: 'min(100vh, 240px)',
53
+ md: 'min(100vh, 360px)',
54
+ lg: 'min(100vh, 480px)',
55
+ xl: 'min(100vh, 640px)',
56
+ full: '100vh',
57
+ };
58
+
59
+ // Anchor + border + radius only — no width/height bake-in. Length is
60
+ // applied via inline `style` from props so vaul's first
61
+ // `getBoundingClientRect()` measurement matches the final layout
62
+ // (avoids the inset.left=mismatch bug when className-based defaults
63
+ // leak into the first paint).
40
64
  const directionStyles = {
41
- bottom: "inset-x-0 bottom-0 mt-24 h-auto rounded-t-lg border-t",
42
- top: "inset-x-0 top-0 mb-24 h-auto rounded-b-lg border-b",
43
- right: "inset-y-0 right-0 h-full w-[280px] border-l",
44
- left: "inset-y-0 left-0 h-full w-[280px] border-r",
65
+ bottom: "inset-x-0 bottom-0 mt-24 rounded-t-lg border-t",
66
+ top: "inset-x-0 top-0 mb-24 rounded-b-lg border-b",
67
+ right: "inset-y-0 right-0 h-full border-l",
68
+ left: "inset-y-0 left-0 h-full border-r",
45
69
  } as const;
46
70
 
71
+ const toCssLength = (value: string | number | undefined): string | undefined => {
72
+ if (value == null) return undefined;
73
+ return typeof value === 'number' ? `${value}px` : value;
74
+ };
75
+
76
+ export interface DrawerContentProps
77
+ extends React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> {
78
+ direction?: 'bottom' | 'right' | 'left' | 'top';
79
+ /** Preset size (default `'md'`). Width for horizontal, height for vertical. */
80
+ size?: DrawerSize;
81
+ /** CSS length for width — applies to left/right drawers. Overrides `size`. */
82
+ width?: string | number;
83
+ /** CSS length for height — applies to top/bottom drawers. Overrides `size`. */
84
+ height?: string | number;
85
+ }
86
+
47
87
  const DrawerContent = React.forwardRef<
48
88
  React.ElementRef<typeof DrawerPrimitive.Content>,
49
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> & { direction?: 'bottom' | 'right' | 'left' | 'top' }
50
- >(({ className, children, direction = 'bottom', style, ...props }, ref) => {
89
+ DrawerContentProps
90
+ >(({ className, children, direction = 'bottom', size = 'md', width, height, style, ...props }, ref) => {
51
91
  const isVertical = direction === 'bottom' || direction === 'top';
52
92
 
93
+ const resolvedWidth = !isVertical
94
+ ? (toCssLength(width) ?? horizontalSizePresets[size])
95
+ : undefined;
96
+ const resolvedHeight = isVertical
97
+ ? (toCssLength(height) ?? verticalSizePresets[size])
98
+ : undefined;
99
+
53
100
  return (
54
101
  <DrawerPortal>
55
102
  <DrawerOverlay />
@@ -62,6 +109,8 @@ const DrawerContent = React.forwardRef<
62
109
  )}
63
110
  style={{
64
111
  transition: 'transform 300ms cubic-bezier(0.32, 0.72, 0, 1)',
112
+ ...(resolvedWidth ? { width: resolvedWidth } : {}),
113
+ ...(resolvedHeight ? { height: resolvedHeight } : {}),
65
114
  ...style,
66
115
  }}
67
116
  {...props}
@@ -83,7 +83,8 @@ function ResponsiveSheetContent({ children, className }: ResponsiveSheetContentP
83
83
  const { isMobile } = React.useContext(ResponsiveSheetContext);
84
84
 
85
85
  if (isMobile) {
86
- return <DrawerContent className={className}>{children}</DrawerContent>;
86
+ // Preserve legacy content-hugging behavior; new Drawer default is `md`.
87
+ return <DrawerContent className={className} height="auto">{children}</DrawerContent>;
87
88
  }
88
89
 
89
90
  return <DialogContent className={className}>{children}</DialogContent>;