@jinaan/uikit 0.1.30 → 0.1.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinaan/uikit",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "description": "Reusable React and Tailwind UI components for Jinaan projects.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
package/src/dialog.tsx CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  useRef,
10
10
  useState,
11
11
  type ComponentPropsWithoutRef,
12
+ type CSSProperties,
12
13
  type ElementRef,
13
14
  type HTMLAttributes,
14
15
  } from "react";
@@ -256,77 +257,117 @@ function startDialogMotion(content: HTMLDivElement, retry = true) {
256
257
  content.addEventListener("animationend", onEnd);
257
258
  }
258
259
 
260
+ export interface DialogContentProps
261
+ extends ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
262
+ /** Vertical placement: centered (default) or anchored near the viewport top. */
263
+ placement?: "center" | "top";
264
+ /** Top edge offset for top placement. Defaults to 11dvh. */
265
+ topOffset?: string;
266
+ }
267
+
268
+ function dialogTopFallback(offset: string): string {
269
+ return offset.replace(/dvh\b/g, "vh");
270
+ }
271
+
259
272
  export const DialogContent = forwardRef<
260
273
  ElementRef<typeof DialogPrimitive.Content>,
261
- ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
262
- >(({ className, children, onEscapeKeyDown, onPointerDownOutside, ...props }, ref) => {
263
- const closing = useContext(DialogClosingContext);
264
- const requestClose = useContext(DialogRequestCloseContext);
265
- const contentRef = useRef<HTMLDivElement>(null);
266
- const setRef = useCallback(
267
- (node: HTMLDivElement | null) => {
268
- contentRef.current = node;
269
- if (node) startDialogMotion(node);
270
- if (typeof ref === "function") {
271
- ref(node);
272
- return;
273
- }
274
- if (ref) {
275
- (ref as { current: HTMLDivElement | null }).current = node;
276
- }
274
+ DialogContentProps
275
+ >(
276
+ (
277
+ {
278
+ className,
279
+ children,
280
+ placement = "center",
281
+ topOffset = "11dvh",
282
+ style,
283
+ onEscapeKeyDown,
284
+ onPointerDownOutside,
285
+ ...props
277
286
  },
278
- [ref],
279
- );
287
+ ref,
288
+ ) => {
289
+ const closing = useContext(DialogClosingContext);
290
+ const top = placement === "top";
291
+ const placementStyle: CSSProperties | undefined = top
292
+ ? ({
293
+ "--dialog-top-offset": topOffset,
294
+ "--dialog-top-offset-fallback": dialogTopFallback(topOffset),
295
+ } as CSSProperties)
296
+ : undefined;
297
+ const requestClose = useContext(DialogRequestCloseContext);
298
+ const contentRef = useRef<HTMLDivElement>(null);
299
+ const setRef = useCallback(
300
+ (node: HTMLDivElement | null) => {
301
+ contentRef.current = node;
302
+ if (node) startDialogMotion(node);
303
+ if (typeof ref === "function") {
304
+ ref(node);
305
+ return;
306
+ }
307
+ if (ref) {
308
+ (ref as { current: HTMLDivElement | null }).current = node;
309
+ }
310
+ },
311
+ [ref],
312
+ );
280
313
 
281
- return (
282
- <DialogPrimitive.Portal>
283
- <DialogPrimitive.Overlay
284
- className={cn(
285
- "fixed inset-0 z-50 overflow-hidden bg-overlay backdrop-blur-[3px] data-[state=open]:animate-in data-[state=open]:fade-in-0",
286
- !closing &&
287
- "data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
288
- closing && "opacity-0 transition-opacity duration-200",
289
- )}
290
- >
291
- <span
292
- aria-hidden="true"
293
- className="dialog-noise pointer-events-none absolute inset-0"
294
- />
295
- </DialogPrimitive.Overlay>
296
- <div className="pointer-events-none fixed inset-0 z-50 flex items-center justify-center overflow-y-auto p-4">
297
- <DialogPrimitive.Content
298
- ref={setRef}
314
+ return (
315
+ <DialogPrimitive.Portal>
316
+ <DialogPrimitive.Overlay
299
317
  className={cn(
300
- "dialog-content pointer-events-auto relative flex max-h-[min(85vh,44rem)] w-full max-w-[22.4rem] flex-col gap-0 overflow-hidden will-change-[transform,opacity]",
301
- "rounded-2xl border border-(--dialog-shell-border) bg-(--dialog-shell-bg) pb-3 shadow-(--dialog-shell-shadow) sm:pb-4",
302
- closing && "dialog-closing",
303
- "focus:outline-none",
304
- className,
318
+ "fixed inset-0 z-50 overflow-hidden bg-overlay backdrop-blur-[3px] data-[state=open]:animate-in data-[state=open]:fade-in-0",
319
+ !closing &&
320
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
321
+ closing && "opacity-0 transition-opacity duration-200",
305
322
  )}
306
- onEscapeKeyDown={(event) => {
307
- onEscapeKeyDown?.(event);
308
- if (event.defaultPrevented) return;
309
- event.preventDefault();
310
- requestClose();
311
- }}
312
- onPointerDownOutside={(event) => {
313
- onPointerDownOutside?.(event);
314
- if (event.defaultPrevented) return;
315
- event.preventDefault();
316
- requestClose();
317
- }}
318
- {...props}
319
323
  >
320
- {children}
321
- <DialogPrimitive.Close className="absolute right-3.5 top-3.5 z-10 rounded-sm text-subtle transition-colors hover:text-fg focus-visible:outline-none">
322
- <X className="size-4" />
323
- <span className="sr-only">Close</span>
324
- </DialogPrimitive.Close>
325
- </DialogPrimitive.Content>
326
- </div>
327
- </DialogPrimitive.Portal>
328
- );
329
- });
324
+ <span
325
+ aria-hidden="true"
326
+ className="dialog-noise pointer-events-none absolute inset-0"
327
+ />
328
+ </DialogPrimitive.Overlay>
329
+ <div
330
+ className={cn(
331
+ "pointer-events-none fixed inset-0 z-50 flex justify-center overflow-y-auto p-4",
332
+ top ? "items-start" : "items-center",
333
+ )}
334
+ >
335
+ <DialogPrimitive.Content
336
+ ref={setRef}
337
+ style={placementStyle ? { ...placementStyle, ...style } : style}
338
+ className={cn(
339
+ "dialog-content pointer-events-auto relative flex max-h-[min(85vh,44rem)] w-full max-w-[22.4rem] flex-col gap-0 overflow-hidden will-change-[transform,opacity]",
340
+ "rounded-2xl border border-(--dialog-shell-border) bg-(--dialog-shell-bg) pb-3 shadow-(--dialog-shell-shadow) sm:pb-4",
341
+ top && "dialog-anchor-top",
342
+ closing && "dialog-closing",
343
+ "focus:outline-none",
344
+ className,
345
+ )}
346
+ onEscapeKeyDown={(event) => {
347
+ onEscapeKeyDown?.(event);
348
+ if (event.defaultPrevented) return;
349
+ event.preventDefault();
350
+ requestClose();
351
+ }}
352
+ onPointerDownOutside={(event) => {
353
+ onPointerDownOutside?.(event);
354
+ if (event.defaultPrevented) return;
355
+ event.preventDefault();
356
+ requestClose();
357
+ }}
358
+ {...props}
359
+ >
360
+ {children}
361
+ <DialogPrimitive.Close className="absolute right-3.5 top-3.5 z-10 rounded-sm text-subtle transition-colors hover:text-fg focus-visible:outline-none">
362
+ <X className="size-4" />
363
+ <span className="sr-only">Close</span>
364
+ </DialogPrimitive.Close>
365
+ </DialogPrimitive.Content>
366
+ </div>
367
+ </DialogPrimitive.Portal>
368
+ );
369
+ },
370
+ );
330
371
  DialogContent.displayName = "DialogContent";
331
372
 
332
373
  /**
package/styles.css CHANGED
@@ -402,6 +402,24 @@
402
402
  }
403
403
  }
404
404
 
405
+ /*
406
+ * Dialog top anchoring: the panel's top edge stays at the requested offset
407
+ * (11dvh by default) regardless of content height. Older browsers without dvh
408
+ * fall back to the equivalent vh value. Unlayered so it wins over the default
409
+ * utility max-height.
410
+ */
411
+ .dialog-anchor-top {
412
+ margin-top: calc(var(--dialog-top-offset-fallback, 11vh) - 1rem);
413
+ max-height: calc(100vh - var(--dialog-top-offset-fallback, 11vh) - 1rem);
414
+ }
415
+
416
+ @supports (height: 100dvh) {
417
+ .dialog-anchor-top {
418
+ margin-top: calc(var(--dialog-top-offset, 11dvh) - 1rem);
419
+ max-height: calc(100dvh - var(--dialog-top-offset, 11dvh) - 1rem);
420
+ }
421
+ }
422
+
405
423
  /* Dialog motion + texture: caller-origin flight, centered fallback, noise. */
406
424
  @keyframes dialog-fly-in {
407
425
  from {