@andreagiugni/tailwind-dashboard-ui 1.0.5 → 1.0.7

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/dist/index.d.cts CHANGED
@@ -541,6 +541,8 @@ interface SelectProps {
541
541
  className?: string;
542
542
  defaultValue?: string;
543
543
  disabled?: boolean;
544
+ /** Render menu under document.body to avoid clipping inside tables/cards. Default: true. */
545
+ portal?: boolean;
544
546
  }
545
547
  /**
546
548
  * A custom single-select dropdown (no native `<select>`), so the menu is fully
@@ -559,6 +561,8 @@ interface MultiSelectProps {
559
561
  defaultSelected?: string[];
560
562
  onChange?: (selected: string[]) => void;
561
563
  disabled?: boolean;
564
+ /** Render menu under document.body to avoid clipping inside tables/cards. Default: true. */
565
+ portal?: boolean;
562
566
  }
563
567
  declare const MultiSelect: React.FC<MultiSelectProps>;
564
568
 
package/dist/index.d.ts CHANGED
@@ -541,6 +541,8 @@ interface SelectProps {
541
541
  className?: string;
542
542
  defaultValue?: string;
543
543
  disabled?: boolean;
544
+ /** Render menu under document.body to avoid clipping inside tables/cards. Default: true. */
545
+ portal?: boolean;
544
546
  }
545
547
  /**
546
548
  * A custom single-select dropdown (no native `<select>`), so the menu is fully
@@ -559,6 +561,8 @@ interface MultiSelectProps {
559
561
  defaultSelected?: string[];
560
562
  onChange?: (selected: string[]) => void;
561
563
  disabled?: boolean;
564
+ /** Render menu under document.body to avoid clipping inside tables/cards. Default: true. */
565
+ portal?: boolean;
562
566
  }
563
567
  declare const MultiSelect: React.FC<MultiSelectProps>;
564
568
 
package/dist/index.js CHANGED
@@ -811,7 +811,7 @@ function DataDrivenTable({
811
811
  }
812
812
  )
813
813
  ] }),
814
- /* @__PURE__ */ jsx("div", { className: "overflow-x-auto border-y border-gray-100 dark:border-white/[0.05]", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full", children: [
814
+ /* @__PURE__ */ jsx("div", { className: "border-y border-gray-100 dark:border-white/[0.05]", children: /* @__PURE__ */ jsxs("table", { className: "min-w-full", children: [
815
815
  /* @__PURE__ */ jsx(TableHeader, { className: "border-b border-gray-100 bg-gray-50 dark:border-white/[0.05] dark:bg-white/[0.06]", children: /* @__PURE__ */ jsx(TableRow, { children: columns.map((col) => {
816
816
  const isActive = sortKey === col.key;
817
817
  return /* @__PURE__ */ jsx(
@@ -2313,15 +2313,20 @@ var Select = ({
2313
2313
  onChange,
2314
2314
  className,
2315
2315
  defaultValue = "",
2316
- disabled = false
2316
+ disabled = false,
2317
+ portal = true
2317
2318
  }) => {
2318
2319
  const [selectedValue, setSelectedValue] = useState(defaultValue);
2319
2320
  const [isOpen, setIsOpen] = useState(false);
2320
2321
  const rootRef = useRef(null);
2322
+ const triggerRef = useRef(null);
2323
+ const menuRef = useRef(null);
2324
+ const [menuPosition, setMenuPosition] = useState();
2321
2325
  useEffect(() => {
2322
2326
  if (!isOpen) return;
2323
2327
  const onPointerDown = (e) => {
2324
- if (rootRef.current && !rootRef.current.contains(e.target)) {
2328
+ const target = e.target;
2329
+ if (rootRef.current && !rootRef.current.contains(target) && !menuRef.current?.contains(target)) {
2325
2330
  setIsOpen(false);
2326
2331
  }
2327
2332
  };
@@ -2335,6 +2340,33 @@ var Select = ({
2335
2340
  document.removeEventListener("keydown", onKeyDown);
2336
2341
  };
2337
2342
  }, [isOpen]);
2343
+ useLayoutEffect(() => {
2344
+ if (!isOpen || !portal) return;
2345
+ const updatePosition = () => {
2346
+ const trigger = triggerRef.current;
2347
+ const menu = menuRef.current;
2348
+ if (!trigger || !menu) return;
2349
+ const rect = trigger.getBoundingClientRect();
2350
+ const menuHeight = menu.offsetHeight;
2351
+ const viewportPadding = 8;
2352
+ const gap = 6;
2353
+ const width = rect.width;
2354
+ const fitsBelow = window.innerHeight - rect.bottom - gap >= menuHeight + viewportPadding;
2355
+ const top = fitsBelow ? rect.bottom + gap : Math.max(viewportPadding, rect.top - gap - menuHeight);
2356
+ const left = Math.min(
2357
+ Math.max(viewportPadding, rect.left),
2358
+ window.innerWidth - width - viewportPadding
2359
+ );
2360
+ setMenuPosition({ top, left, width });
2361
+ };
2362
+ updatePosition();
2363
+ window.addEventListener("resize", updatePosition);
2364
+ window.addEventListener("scroll", updatePosition, true);
2365
+ return () => {
2366
+ window.removeEventListener("resize", updatePosition);
2367
+ window.removeEventListener("scroll", updatePosition, true);
2368
+ };
2369
+ }, [isOpen, portal]);
2338
2370
  const toggleOpen = () => {
2339
2371
  if (!disabled) setIsOpen((prev) => !prev);
2340
2372
  };
@@ -2348,6 +2380,7 @@ var Select = ({
2348
2380
  /* @__PURE__ */ jsxs(
2349
2381
  "button",
2350
2382
  {
2383
+ ref: triggerRef,
2351
2384
  type: "button",
2352
2385
  onClick: toggleOpen,
2353
2386
  disabled,
@@ -2383,52 +2416,61 @@ var Select = ({
2383
2416
  ]
2384
2417
  }
2385
2418
  ),
2386
- isOpen && /* @__PURE__ */ jsx(
2387
- "ul",
2388
- {
2389
- role: "listbox",
2390
- className: cn(
2391
- "absolute left-0 top-full mt-1.5 max-h-60 w-full overflow-y-auto rounded-lg border border-gray-200 bg-white py-1 shadow-theme-lg dark:border-gray-800 dark:bg-gray-900",
2392
- layers.menu
2393
- ),
2394
- children: options.map((option) => {
2395
- const isSelected = option.value === selectedValue;
2396
- return /* @__PURE__ */ jsxs(
2397
- "li",
2398
- {
2399
- role: "option",
2400
- "aria-selected": isSelected,
2401
- onClick: () => handleSelect(option.value),
2402
- className: cn(
2403
- "flex cursor-pointer items-center justify-between px-4 py-2 text-sm transition hover:bg-gray-50 dark:hover:bg-white/5",
2404
- isSelected ? "bg-brand-50 font-medium text-brand-600 dark:bg-brand-500/15 dark:text-brand-400" : "text-gray-700 dark:text-gray-300"
2405
- ),
2406
- children: [
2407
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [
2408
- option.icon && /* @__PURE__ */ jsx(
2409
- "span",
2419
+ isOpen && (() => {
2420
+ const menu = /* @__PURE__ */ jsx(
2421
+ "ul",
2422
+ {
2423
+ ref: menuRef,
2424
+ role: "listbox",
2425
+ className: cn(
2426
+ "max-h-60 overflow-y-auto rounded-lg border border-gray-200 bg-white py-1 shadow-theme-lg dark:border-gray-800 dark:bg-gray-900",
2427
+ portal ? cn("fixed", layers.portal) : cn("absolute left-0 top-full mt-1.5 w-full", layers.menu)
2428
+ ),
2429
+ style: {
2430
+ ...portal && !menuPosition ? { visibility: "hidden" } : void 0,
2431
+ ...portal ? menuPosition : void 0
2432
+ },
2433
+ children: options.map((option) => {
2434
+ const isSelected = option.value === selectedValue;
2435
+ return /* @__PURE__ */ jsxs(
2436
+ "li",
2437
+ {
2438
+ role: "option",
2439
+ "aria-selected": isSelected,
2440
+ onClick: () => handleSelect(option.value),
2441
+ className: cn(
2442
+ "flex cursor-pointer items-center justify-between px-4 py-2 text-sm transition hover:bg-gray-50 dark:hover:bg-white/5",
2443
+ isSelected ? "bg-brand-50 font-medium text-brand-600 dark:bg-brand-500/15 dark:text-brand-400" : "text-gray-700 dark:text-gray-300"
2444
+ ),
2445
+ children: [
2446
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [
2447
+ option.icon && /* @__PURE__ */ jsx(
2448
+ "span",
2449
+ {
2450
+ "aria-hidden": "true",
2451
+ className: "flex h-5 w-5 shrink-0 items-center justify-center [&>svg]:size-4",
2452
+ children: option.icon
2453
+ }
2454
+ ),
2455
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: option.label })
2456
+ ] }),
2457
+ isSelected && /* @__PURE__ */ jsx(
2458
+ Check,
2410
2459
  {
2411
- "aria-hidden": "true",
2412
- className: "flex h-5 w-5 shrink-0 items-center justify-center [&>svg]:size-4",
2413
- children: option.icon
2460
+ className: "ml-2 size-4 shrink-0 stroke-current",
2461
+ "aria-hidden": "true"
2414
2462
  }
2415
- ),
2416
- /* @__PURE__ */ jsx("span", { className: "truncate", children: option.label })
2417
- ] }),
2418
- isSelected && /* @__PURE__ */ jsx(
2419
- Check,
2420
- {
2421
- className: "ml-2 size-4 shrink-0 stroke-current",
2422
- "aria-hidden": "true"
2423
- }
2424
- )
2425
- ]
2426
- },
2427
- option.value
2428
- );
2429
- })
2430
- }
2431
- )
2463
+ )
2464
+ ]
2465
+ },
2466
+ option.value
2467
+ );
2468
+ })
2469
+ }
2470
+ );
2471
+ if (!portal || typeof document === "undefined") return menu;
2472
+ return createPortal(menu, document.body);
2473
+ })()
2432
2474
  ] });
2433
2475
  };
2434
2476
  var MultiSelect = ({
@@ -2436,10 +2478,60 @@ var MultiSelect = ({
2436
2478
  options,
2437
2479
  defaultSelected = [],
2438
2480
  onChange,
2439
- disabled = false
2481
+ disabled = false,
2482
+ portal = true
2440
2483
  }) => {
2441
2484
  const [selectedOptions, setSelectedOptions] = useState(defaultSelected);
2442
2485
  const [isOpen, setIsOpen] = useState(false);
2486
+ const rootRef = useRef(null);
2487
+ const triggerRef = useRef(null);
2488
+ const menuRef = useRef(null);
2489
+ const [menuPosition, setMenuPosition] = useState();
2490
+ useEffect(() => {
2491
+ if (!isOpen) return;
2492
+ const onPointerDown = (event) => {
2493
+ const target = event.target;
2494
+ if (rootRef.current && !rootRef.current.contains(target) && !menuRef.current?.contains(target)) {
2495
+ setIsOpen(false);
2496
+ }
2497
+ };
2498
+ const onKeyDown = (event) => {
2499
+ if (event.key === "Escape") setIsOpen(false);
2500
+ };
2501
+ document.addEventListener("mousedown", onPointerDown);
2502
+ document.addEventListener("keydown", onKeyDown);
2503
+ return () => {
2504
+ document.removeEventListener("mousedown", onPointerDown);
2505
+ document.removeEventListener("keydown", onKeyDown);
2506
+ };
2507
+ }, [isOpen]);
2508
+ useLayoutEffect(() => {
2509
+ if (!isOpen || !portal) return;
2510
+ const updatePosition = () => {
2511
+ const trigger = triggerRef.current;
2512
+ const menu = menuRef.current;
2513
+ if (!trigger || !menu) return;
2514
+ const rect = trigger.getBoundingClientRect();
2515
+ const menuHeight = menu.offsetHeight;
2516
+ const viewportPadding = 8;
2517
+ const gap = 6;
2518
+ const width = rect.width;
2519
+ const fitsBelow = window.innerHeight - rect.bottom - gap >= menuHeight + viewportPadding;
2520
+ const top = fitsBelow ? rect.bottom + gap : Math.max(viewportPadding, rect.top - gap - menuHeight);
2521
+ const left = Math.min(
2522
+ Math.max(viewportPadding, rect.left),
2523
+ window.innerWidth - width - viewportPadding
2524
+ );
2525
+ setMenuPosition({ top, left, width });
2526
+ };
2527
+ updatePosition();
2528
+ window.addEventListener("resize", updatePosition);
2529
+ window.addEventListener("scroll", updatePosition, true);
2530
+ return () => {
2531
+ window.removeEventListener("resize", updatePosition);
2532
+ window.removeEventListener("scroll", updatePosition, true);
2533
+ };
2534
+ }, [isOpen, portal]);
2443
2535
  const toggleDropdown = () => {
2444
2536
  if (disabled) return;
2445
2537
  setIsOpen((prev) => !prev);
@@ -2457,94 +2549,110 @@ var MultiSelect = ({
2457
2549
  const selectedValuesText = selectedOptions.map(
2458
2550
  (value) => options.find((option) => option.value === value)?.text || ""
2459
2551
  );
2460
- return /* @__PURE__ */ jsxs("div", { className: "w-full", children: [
2552
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: "w-full", children: [
2461
2553
  /* @__PURE__ */ jsx("label", { className: "mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400", children: label }),
2462
2554
  /* @__PURE__ */ jsx("div", { className: "relative inline-block w-full", children: /* @__PURE__ */ jsxs("div", { className: "relative flex flex-col items-center", children: [
2463
- /* @__PURE__ */ jsx("div", { onClick: toggleDropdown, className: "w-full", children: /* @__PURE__ */ jsxs("div", { className: "mb-2 flex h-11 rounded-lg border border-gray-300 bg-white py-1.5 px-4 shadow-theme-xs outline-hidden transition focus:border-brand-300 focus:shadow-focus-ring dark:border-gray-700 dark:bg-gray-900 dark:focus:border-brand-300", children: [
2464
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap flex-auto gap-2", children: selectedValuesText.length > 0 ? selectedValuesText.map((text, index) => /* @__PURE__ */ jsxs(
2465
- "div",
2466
- {
2467
- className: "group flex items-center justify-center rounded-full border-[0.7px] border-transparent bg-gray-100 py-1 pl-2.5 pr-2 text-sm text-gray-800 hover:border-gray-200 dark:bg-gray-800 dark:text-white/90 dark:hover:border-gray-800",
2468
- children: [
2469
- /* @__PURE__ */ jsx("span", { className: "flex-initial max-w-full", children: text }),
2470
- /* @__PURE__ */ jsx("div", { className: "flex flex-row-reverse flex-auto", children: /* @__PURE__ */ jsx(
2471
- "div",
2472
- {
2473
- onClick: () => removeOption(index, selectedOptions[index]),
2474
- className: "pl-2 text-gray-500 cursor-pointer group-hover:text-gray-400 dark:text-gray-400",
2475
- children: /* @__PURE__ */ jsx(X, { className: "size-4", role: "button", "aria-hidden": "true" })
2476
- }
2477
- ) })
2478
- ]
2479
- },
2480
- index
2481
- )) : /* @__PURE__ */ jsx(
2482
- "input",
2483
- {
2484
- placeholder: "Select option",
2485
- className: "w-full h-full p-1 pr-2 text-sm bg-transparent border-0 outline-hidden appearance-none placeholder:text-gray-800 focus:border-0 focus:outline-hidden focus:ring-0 dark:placeholder:text-white/90",
2486
- readOnly: true,
2487
- value: "Select option"
2488
- }
2489
- ) }),
2490
- /* @__PURE__ */ jsx("div", { className: "flex items-center py-1 pl-1 pr-1 w-7", children: /* @__PURE__ */ jsx(
2491
- "button",
2492
- {
2493
- type: "button",
2494
- onClick: toggleDropdown,
2495
- className: "w-5 h-5 text-gray-700 outline-hidden cursor-pointer focus:outline-hidden dark:text-gray-400",
2496
- children: /* @__PURE__ */ jsx(
2497
- ChevronDown,
2498
- {
2499
- className: `size-4 stroke-current ${isOpen ? "rotate-180" : ""}`,
2500
- "aria-hidden": "true"
2501
- }
2502
- )
2503
- }
2504
- ) })
2505
- ] }) }),
2506
- isOpen && /* @__PURE__ */ jsx(
2555
+ /* @__PURE__ */ jsx("div", { onClick: toggleDropdown, className: "w-full", children: /* @__PURE__ */ jsxs(
2507
2556
  "div",
2508
2557
  {
2509
- className: cn(
2510
- "absolute left-0 top-full max-h-select w-full overflow-y-auto rounded-lg bg-white shadow-sm dark:bg-gray-900",
2511
- layers.menu
2512
- ),
2513
- onClick: (e) => e.stopPropagation(),
2514
- children: /* @__PURE__ */ jsx("div", { className: "flex flex-col", children: options.map((option, index) => {
2515
- const isSelected = selectedOptions.includes(option.value);
2516
- return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
2558
+ ref: triggerRef,
2559
+ className: "mb-2 flex h-11 rounded-lg border border-gray-300 bg-white py-1.5 px-4 shadow-theme-xs outline-hidden transition focus:border-brand-300 focus:shadow-focus-ring dark:border-gray-700 dark:bg-gray-900 dark:focus:border-brand-300",
2560
+ children: [
2561
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap flex-auto gap-2", children: selectedValuesText.length > 0 ? selectedValuesText.map((text, index) => /* @__PURE__ */ jsxs(
2517
2562
  "div",
2518
2563
  {
2519
- className: `w-full cursor-pointer rounded-t border-b border-gray-200 hover:bg-brand-500/5 dark:border-gray-800`,
2520
- onClick: () => handleSelect(option.value),
2521
- children: /* @__PURE__ */ jsxs(
2522
- "div",
2564
+ className: "group flex items-center justify-center rounded-full border-[0.7px] border-transparent bg-gray-100 py-1 pl-2.5 pr-2 text-sm text-gray-800 hover:border-gray-200 dark:bg-gray-800 dark:text-white/90 dark:hover:border-gray-800",
2565
+ children: [
2566
+ /* @__PURE__ */ jsx("span", { className: "flex-initial max-w-full", children: text }),
2567
+ /* @__PURE__ */ jsx("div", { className: "flex flex-row-reverse flex-auto", children: /* @__PURE__ */ jsx(
2568
+ "div",
2569
+ {
2570
+ onClick: () => removeOption(index, selectedOptions[index]),
2571
+ className: "pl-2 text-gray-500 cursor-pointer group-hover:text-gray-400 dark:text-gray-400",
2572
+ children: /* @__PURE__ */ jsx(X, { className: "size-4", role: "button", "aria-hidden": "true" })
2573
+ }
2574
+ ) })
2575
+ ]
2576
+ },
2577
+ index
2578
+ )) : /* @__PURE__ */ jsx(
2579
+ "input",
2580
+ {
2581
+ placeholder: "Select option",
2582
+ className: "w-full h-full p-1 pr-2 text-sm bg-transparent border-0 outline-hidden appearance-none placeholder:text-gray-800 focus:border-0 focus:outline-hidden focus:ring-0 dark:placeholder:text-white/90",
2583
+ readOnly: true,
2584
+ value: "Select option"
2585
+ }
2586
+ ) }),
2587
+ /* @__PURE__ */ jsx("div", { className: "flex items-center py-1 pl-1 pr-1 w-7", children: /* @__PURE__ */ jsx(
2588
+ "button",
2589
+ {
2590
+ type: "button",
2591
+ onClick: toggleDropdown,
2592
+ className: "w-5 h-5 text-gray-700 outline-hidden cursor-pointer focus:outline-hidden dark:text-gray-400",
2593
+ children: /* @__PURE__ */ jsx(
2594
+ ChevronDown,
2523
2595
  {
2524
- className: `relative flex w-full items-center justify-between p-2 pl-2 ${isSelected ? "bg-brand-500/10" : ""}`,
2525
- children: [
2526
- /* @__PURE__ */ jsx(
2527
- "div",
2528
- {
2529
- className: `mx-2 leading-6 ${isSelected ? "font-medium text-brand-600 dark:text-brand-400" : "text-gray-800 dark:text-white/90"}`,
2530
- children: option.text
2531
- }
2532
- ),
2533
- isSelected && /* @__PURE__ */ jsx(
2534
- Check,
2535
- {
2536
- className: "mr-2 size-4 shrink-0 stroke-current text-brand-500",
2537
- "aria-hidden": "true"
2538
- }
2539
- )
2540
- ]
2596
+ className: `size-4 stroke-current ${isOpen ? "rotate-180" : ""}`,
2597
+ "aria-hidden": "true"
2541
2598
  }
2542
2599
  )
2543
2600
  }
2544
- ) }, index);
2545
- }) })
2601
+ ) })
2602
+ ]
2546
2603
  }
2547
- )
2604
+ ) }),
2605
+ isOpen && (() => {
2606
+ const menu = /* @__PURE__ */ jsx(
2607
+ "div",
2608
+ {
2609
+ ref: menuRef,
2610
+ className: cn(
2611
+ "max-h-select overflow-y-auto rounded-lg bg-white shadow-sm dark:bg-gray-900",
2612
+ portal ? cn("fixed", layers.portal) : cn("absolute left-0 top-full w-full", layers.menu)
2613
+ ),
2614
+ style: {
2615
+ ...portal && !menuPosition ? { visibility: "hidden" } : void 0,
2616
+ ...portal ? menuPosition : void 0
2617
+ },
2618
+ onClick: (e) => e.stopPropagation(),
2619
+ children: /* @__PURE__ */ jsx("div", { className: "flex flex-col", children: options.map((option, index) => {
2620
+ const isSelected = selectedOptions.includes(option.value);
2621
+ return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
2622
+ "div",
2623
+ {
2624
+ className: `w-full cursor-pointer rounded-t border-b border-gray-200 hover:bg-brand-500/5 dark:border-gray-800`,
2625
+ onClick: () => handleSelect(option.value),
2626
+ children: /* @__PURE__ */ jsxs(
2627
+ "div",
2628
+ {
2629
+ className: `relative flex w-full items-center justify-between p-2 pl-2 ${isSelected ? "bg-brand-500/10" : ""}`,
2630
+ children: [
2631
+ /* @__PURE__ */ jsx(
2632
+ "div",
2633
+ {
2634
+ className: `mx-2 leading-6 ${isSelected ? "font-medium text-brand-600 dark:text-brand-400" : "text-gray-800 dark:text-white/90"}`,
2635
+ children: option.text
2636
+ }
2637
+ ),
2638
+ isSelected && /* @__PURE__ */ jsx(
2639
+ Check,
2640
+ {
2641
+ className: "mr-2 size-4 shrink-0 stroke-current text-brand-500",
2642
+ "aria-hidden": "true"
2643
+ }
2644
+ )
2645
+ ]
2646
+ }
2647
+ )
2648
+ }
2649
+ ) }, index);
2650
+ }) })
2651
+ }
2652
+ );
2653
+ if (!portal || typeof document === "undefined") return menu;
2654
+ return createPortal(menu, document.body);
2655
+ })()
2548
2656
  ] }) })
2549
2657
  ] });
2550
2658
  };