@andreagiugni/tailwind-dashboard-ui 1.0.22 → 1.0.23

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
@@ -115,6 +115,7 @@ import {
115
115
  Drawer,
116
116
  Chip,
117
117
  Progress,
118
+ PollResults,
118
119
  Spinner,
119
120
  Skeleton,
120
121
  Slider,
@@ -181,6 +182,7 @@ Legend: **(+native)** = also extends the element's native HTML attributes.
181
182
  | `Code` | `color` (primary/success/error/warning/info/default), `size` (sm/md), (+native code) (+override) |
182
183
  | `Snippet` | `code` (string \| string[]), `symbol?`, `hideCopyButton?`, `hideSymbol?`, `size`, (+override) |
183
184
  | `Progress` | `value`, `maxValue?`, `color`, `size` (sm/md/lg), `label?`, `showValueLabel?`, `isIndeterminate?` |
185
+ | `PollResults` | `options` (`{ label, value?, series? }[]`), `maxValue?`, `size` (sm/md/lg), `color`, `trackColor?`, `showValueLabel?`, `showSeriesLabels?`, `valueFormatter?`, (+native div) (+override) |
184
186
  | `Spinner` | `size` (sm/md/lg), `color`, `label?` |
185
187
  | `Skeleton` | `isLoaded`, `width?`, `height?`, `className`, `children?` |
186
188
  | `Toast` | `variant` (success/danger/warning/info), `title`, `message?`, `icon?`, `hideIcon?`, `showCloseButton?`, `onClose?`, `hideAccentBar?`, (+native div) (+override) |
@@ -190,7 +192,7 @@ Legend: **(+native)** = also extends the element's native HTML attributes.
190
192
  | Component | Key props |
191
193
  |---|---|
192
194
  | `Tooltip` | `content`, `placement` (top/right/bottom/left), `delay?`, `disabled?`, (+native span) (+override) |
193
- | `Popover` | `trigger`, `placement`, `isOpen?` / `defaultOpen?` / `onOpenChange?`, `closeOnOutsideClick?`, `closeOnEsc?` |
195
+ | `Popover` | `trigger`, `placement`, `isOpen?` / `defaultOpen?` / `onOpenChange?`, `closeOnOutsideClick?`, `closeOnEsc?`, `portal?` (default `true`, prevents clipping inside cards/overflow containers) |
194
196
  | `Drawer` | `isOpen`, `onClose`, `placement` (left/right/top/bottom), `showCloseButton?`, `closeOnBackdrop?`, `closeOnEsc?`, `size?`, (+native div) |
195
197
 
196
198
  ### Form controls
@@ -208,8 +210,8 @@ Legend: **(+native)** = also extends the element's native HTML attributes.
208
210
  | `FileInput` | `onChange`, `accept`, `multiple`, (+native input) |
209
211
  | `PasswordInput` | password field with a show/hide eye toggle, (+native input attrs) |
210
212
  | `Dropzone` | `onDrop(files)`, `accept?`, `multiple?`, `title?`, `description?`, `browseLabel?` — drag-and-drop upload, dependency-free |
211
- | `DatePicker` | `id`, `mode` (single/multiple/range/time), `defaultDate`, `onChange`, `label`, `placeholder` |
212
- | `DateTimePicker` | `id`, `label?`, `placeholder?`, `defaultDate?`, `defaultTime?` (`"HH:MM"`), `onChange?({ date, time })` — flatpickr date calendar + a compact `HH:MM` time input |
213
+ | `DatePicker` | `id`, `mode` (single/multiple/range/time), `defaultDate`, `onChange`, `label`, `placeholder` — calendar is appended to `document.body` to avoid clipping inside cards/overflow containers |
214
+ | `DateTimePicker` | `id`, `label?`, `placeholder?`, `defaultDate?`, `defaultTime?` (`"HH:MM"`), `onChange?({ date, time })` — flatpickr date calendar appended to `document.body` + a compact `HH:MM` time input |
213
215
  | `Slider` | `value` / `defaultValue`, `onChange(value)`, `min`, `max`, `step`, `color`, `label?`, `showValue?`, `disabled` |
214
216
  | `ColorPicker` | `defaultValue?` (hex), `defaultFormat?` / `formats?` (`hex`/`rgb`/`rgba`/`hsl`/`hsla`), `showInput?`, `withEyeDropper?`, `label?`, `onChange?(value, color)` — compact swatch + formatted value trigger opening a saturation/hue/alpha popup with a value input, format switcher and clear-value button; built on `react-beautiful-color` (bundled), client-only, **import its CSS** (see ColorPicker note above) |
215
217
  | `Editor` (WYSIWYG) | `content`, `onChange(html)`, `placeholder?`, `editable?`, `toolbar?` (ordered tool ids or custom buttons), `rows?` (minimum visible text rows, default `8`), `editorClassName?` — optional Tiptap peers; render client-only (see Data viz) |
package/dist/index.cjs CHANGED
@@ -1489,12 +1489,6 @@ var Tooltip = ({
1489
1489
  }
1490
1490
  );
1491
1491
  };
1492
- var placementClasses2 = {
1493
- top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
1494
- right: "left-full top-1/2 -translate-y-1/2 ml-2",
1495
- bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
1496
- left: "right-full top-1/2 -translate-y-1/2 mr-2"
1497
- };
1498
1492
  var Popover = ({
1499
1493
  trigger,
1500
1494
  placement = "bottom",
@@ -1503,14 +1497,17 @@ var Popover = ({
1503
1497
  defaultOpen = false,
1504
1498
  closeOnOutsideClick = true,
1505
1499
  closeOnEsc = true,
1500
+ portal = true,
1506
1501
  children,
1507
1502
  className,
1508
1503
  ...rest
1509
1504
  }) => {
1510
1505
  const [uncontrolledOpen, setUncontrolledOpen] = React5.useState(defaultOpen);
1506
+ const [panelPosition, setPanelPosition] = React5.useState();
1511
1507
  const isControlled = controlledOpen !== void 0;
1512
1508
  const open = isControlled ? controlledOpen : uncontrolledOpen;
1513
1509
  const wrapperRef = React5.useRef(null);
1510
+ const panelRef = React5.useRef(null);
1514
1511
  const setOpen = (next) => {
1515
1512
  if (!isControlled) setUncontrolledOpen(next);
1516
1513
  onOpenChange?.(next);
@@ -1518,7 +1515,7 @@ var Popover = ({
1518
1515
  React5.useEffect(() => {
1519
1516
  if (!open) return;
1520
1517
  const handleClickOutside = (event) => {
1521
- if (closeOnOutsideClick && wrapperRef.current && !wrapperRef.current.contains(event.target)) {
1518
+ if (closeOnOutsideClick && wrapperRef.current && !wrapperRef.current.contains(event.target) && !panelRef.current?.contains(event.target)) {
1522
1519
  setOpen(false);
1523
1520
  }
1524
1521
  };
@@ -1534,6 +1531,82 @@ var Popover = ({
1534
1531
  document.removeEventListener("keydown", handleEscape);
1535
1532
  };
1536
1533
  }, [open, closeOnOutsideClick, closeOnEsc]);
1534
+ const updatePanelPosition = React5.useCallback(() => {
1535
+ const trigger2 = wrapperRef.current;
1536
+ const panel2 = panelRef.current;
1537
+ if (!trigger2 || !panel2) return;
1538
+ const rect = trigger2.getBoundingClientRect();
1539
+ const panelWidth = panel2.offsetWidth;
1540
+ const panelHeight = panel2.offsetHeight;
1541
+ const viewportPadding = 8;
1542
+ const gap = 8;
1543
+ const centeredLeft = rect.left + rect.width / 2 - panelWidth / 2;
1544
+ const centeredTop = rect.top + rect.height / 2 - panelHeight / 2;
1545
+ const canFitAbove = rect.top - gap - panelHeight >= viewportPadding;
1546
+ const canFitBelow = rect.bottom + gap + panelHeight <= window.innerHeight - viewportPadding;
1547
+ const canFitLeft = rect.left - gap - panelWidth >= viewportPadding;
1548
+ const canFitRight = rect.right + gap + panelWidth <= window.innerWidth - viewportPadding;
1549
+ let effectivePlacement = placement;
1550
+ if (placement === "bottom" && !canFitBelow && canFitAbove) {
1551
+ effectivePlacement = "top";
1552
+ } else if (placement === "top" && !canFitAbove && canFitBelow) {
1553
+ effectivePlacement = "bottom";
1554
+ } else if (placement === "left" && !canFitLeft && canFitRight) {
1555
+ effectivePlacement = "right";
1556
+ } else if (placement === "right" && !canFitRight && canFitLeft) {
1557
+ effectivePlacement = "left";
1558
+ }
1559
+ let top = rect.bottom + gap;
1560
+ let left = centeredLeft;
1561
+ if (effectivePlacement === "top") {
1562
+ top = rect.top - gap - panelHeight;
1563
+ left = centeredLeft;
1564
+ } else if (effectivePlacement === "right") {
1565
+ top = centeredTop;
1566
+ left = rect.right + gap;
1567
+ } else if (effectivePlacement === "left") {
1568
+ top = centeredTop;
1569
+ left = rect.left - gap - panelWidth;
1570
+ }
1571
+ setPanelPosition({
1572
+ top: Math.min(
1573
+ Math.max(viewportPadding, top),
1574
+ window.innerHeight - panelHeight - viewportPadding
1575
+ ),
1576
+ left: Math.min(
1577
+ Math.max(viewportPadding, left),
1578
+ window.innerWidth - panelWidth - viewportPadding
1579
+ )
1580
+ });
1581
+ }, [placement]);
1582
+ React5.useLayoutEffect(() => {
1583
+ if (!open || !portal) return;
1584
+ updatePanelPosition();
1585
+ window.addEventListener("resize", updatePanelPosition);
1586
+ window.addEventListener("scroll", updatePanelPosition, true);
1587
+ return () => {
1588
+ window.removeEventListener("resize", updatePanelPosition);
1589
+ window.removeEventListener("scroll", updatePanelPosition, true);
1590
+ };
1591
+ }, [open, portal, updatePanelPosition]);
1592
+ const panel = open ? /* @__PURE__ */ jsxRuntime.jsx(
1593
+ "div",
1594
+ {
1595
+ ref: panelRef,
1596
+ role: "dialog",
1597
+ className: chunkYERNSNT4_cjs.cn(
1598
+ "min-w-48 rounded-xl border border-gray-200 bg-white p-4 shadow-theme-lg dark:border-gray-700 dark:bg-gray-900 dark:text-white/90",
1599
+ portal ? chunkYERNSNT4_cjs.cn("fixed", chunkTF3G3E72_cjs.layers.portal) : chunkYERNSNT4_cjs.cn("absolute top-full left-1/2 mt-2 -translate-x-1/2", chunkTF3G3E72_cjs.layers.floating),
1600
+ className
1601
+ ),
1602
+ style: {
1603
+ ...portal && !panelPosition ? { visibility: "hidden" } : void 0,
1604
+ ...portal ? panelPosition : void 0
1605
+ },
1606
+ ...rest,
1607
+ children
1608
+ }
1609
+ ) : null;
1537
1610
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: wrapperRef, className: "relative inline-block", children: [
1538
1611
  /* @__PURE__ */ jsxRuntime.jsx(
1539
1612
  "span",
@@ -1545,20 +1618,7 @@ var Popover = ({
1545
1618
  children: trigger
1546
1619
  }
1547
1620
  ),
1548
- open && /* @__PURE__ */ jsxRuntime.jsx(
1549
- "div",
1550
- {
1551
- role: "dialog",
1552
- className: chunkYERNSNT4_cjs.cn(
1553
- "absolute min-w-[12rem] rounded-xl border border-gray-200 bg-white p-4 shadow-theme-lg dark:border-gray-700 dark:bg-gray-900 dark:text-white/90",
1554
- chunkTF3G3E72_cjs.layers.floating,
1555
- placementClasses2[placement],
1556
- className
1557
- ),
1558
- ...rest,
1559
- children
1560
- }
1561
- )
1621
+ portal && typeof document !== "undefined" ? panel && reactDom.createPortal(panel, document.body) : panel
1562
1622
  ] });
1563
1623
  };
1564
1624
  var panelBaseByPlacement = {
@@ -1818,12 +1878,134 @@ var Progress = ({
1818
1878
  )
1819
1879
  ] });
1820
1880
  };
1881
+ var trackSizes = {
1882
+ sm: "h-2",
1883
+ md: "h-2.5",
1884
+ lg: "h-3.5"
1885
+ };
1886
+ var colorStyles = {
1887
+ primary: "bg-brand-500",
1888
+ success: "bg-success-500",
1889
+ error: "bg-error-500",
1890
+ warning: "bg-warning-500",
1891
+ info: "bg-blue-light-500",
1892
+ light: "bg-gray-400",
1893
+ dark: "bg-gray-700 dark:bg-gray-300"
1894
+ };
1895
+ var defaultColorCycle = [
1896
+ "primary",
1897
+ "success",
1898
+ "info",
1899
+ "warning",
1900
+ "error",
1901
+ "dark"
1902
+ ];
1903
+ var defaultSeriesColorCycle = ["primary", "success"];
1904
+ var clampValue = (value, maxValue) => {
1905
+ return Math.min(Math.max(value, 0), maxValue);
1906
+ };
1907
+ var toPercent = (value, maxValue) => {
1908
+ return Math.round(clampValue(value, maxValue) / maxValue * 100);
1909
+ };
1910
+ var nodeToLabel = (node) => {
1911
+ if (typeof node === "string" || typeof node === "number") {
1912
+ return String(node);
1913
+ }
1914
+ return void 0;
1915
+ };
1916
+ var defaultValueFormatter = (_value, percent) => `${percent}%`;
1917
+ var PollResults = ({
1918
+ options,
1919
+ maxValue = 100,
1920
+ size = "md",
1921
+ color = "primary",
1922
+ trackColor,
1923
+ showValueLabel = true,
1924
+ showSeriesLabels = true,
1925
+ valueFormatter = defaultValueFormatter,
1926
+ className,
1927
+ bgColor,
1928
+ textColor,
1929
+ borderColor,
1930
+ style,
1931
+ ...rest
1932
+ }) => {
1933
+ const safeMax = maxValue <= 0 ? 100 : maxValue;
1934
+ return /* @__PURE__ */ jsxRuntime.jsx(
1935
+ "div",
1936
+ {
1937
+ className: chunkYERNSNT4_cjs.cn("w-full space-y-7 text-gray-800 dark:text-white/90", className),
1938
+ style: { ...styleOverride({ bgColor, textColor, borderColor }), ...style },
1939
+ ...rest,
1940
+ children: options.map((option, optionIndex) => {
1941
+ const optionText = nodeToLabel(option.label);
1942
+ const series = option.series?.length ? option.series : [
1943
+ {
1944
+ value: option.value ?? 0,
1945
+ color: option.color ?? defaultColorCycle[optionIndex % defaultColorCycle.length] ?? color,
1946
+ fillColor: option.fillColor,
1947
+ ariaLabel: option.ariaLabel
1948
+ }
1949
+ ];
1950
+ const isGrouped = series.length > 1;
1951
+ const singlePercent = toPercent(series[0]?.value ?? 0, safeMax);
1952
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1953
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3 flex items-center justify-between gap-4", children: [
1954
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 break-words text-xl font-semibold leading-snug text-gray-800 dark:text-white/90", children: option.label }),
1955
+ !isGrouped && showValueLabel && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-xl font-semibold leading-snug text-gray-600 dark:text-gray-300", children: valueFormatter(series[0]?.value ?? 0, singlePercent) })
1956
+ ] }),
1957
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: chunkYERNSNT4_cjs.cn("flex flex-col", isGrouped ? "gap-3" : "gap-0"), children: series.map((entry, seriesIndex) => {
1958
+ const percent = toPercent(entry.value, safeMax);
1959
+ const fillColor = entry.fillColor;
1960
+ const entryColor = entry.color ?? (isGrouped ? defaultSeriesColorCycle[seriesIndex % defaultSeriesColorCycle.length] : option.color ?? defaultColorCycle[optionIndex % defaultColorCycle.length]) ?? color;
1961
+ const entryLabel = nodeToLabel(entry.label);
1962
+ const ariaLabel = entry.ariaLabel ?? [optionText, isGrouped ? entryLabel : void 0].filter(Boolean).join(" ");
1963
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1964
+ isGrouped && (showSeriesLabels || showValueLabel) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-1.5 flex items-center justify-between gap-3", children: [
1965
+ showSeriesLabels ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 break-words text-xs font-medium text-gray-500 dark:text-gray-400", children: entry.label }) : /* @__PURE__ */ jsxRuntime.jsx("span", {}),
1966
+ showValueLabel && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-xs font-semibold text-gray-600 dark:text-gray-300", children: valueFormatter(entry.value, percent) })
1967
+ ] }),
1968
+ /* @__PURE__ */ jsxRuntime.jsx(
1969
+ "div",
1970
+ {
1971
+ role: "progressbar",
1972
+ "aria-label": ariaLabel || void 0,
1973
+ "aria-valuemin": 0,
1974
+ "aria-valuemax": safeMax,
1975
+ "aria-valuenow": clampValue(entry.value, safeMax),
1976
+ className: chunkYERNSNT4_cjs.cn(
1977
+ "relative w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800",
1978
+ trackSizes[size]
1979
+ ),
1980
+ style: trackColor ? { backgroundColor: trackColor } : void 0,
1981
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1982
+ "div",
1983
+ {
1984
+ className: chunkYERNSNT4_cjs.cn(
1985
+ "h-full rounded-full transition-all duration-300",
1986
+ fillColor ? void 0 : colorStyles[entryColor]
1987
+ ),
1988
+ style: {
1989
+ width: `${percent}%`,
1990
+ ...fillColor ? { backgroundColor: fillColor } : {}
1991
+ }
1992
+ }
1993
+ )
1994
+ }
1995
+ )
1996
+ ] }, `${entryLabel ?? "series"}-${seriesIndex}`);
1997
+ }) })
1998
+ ] }, `${optionText ?? "poll-option"}-${optionIndex}`);
1999
+ })
2000
+ }
2001
+ );
2002
+ };
1821
2003
  var sizeStyles4 = {
1822
2004
  sm: "h-4 w-4 border-2",
1823
2005
  md: "h-6 w-6 border-2",
1824
2006
  lg: "h-8 w-8 border-[3px]"
1825
2007
  };
1826
- var colorStyles = {
2008
+ var colorStyles2 = {
1827
2009
  primary: "text-brand-500",
1828
2010
  success: "text-success-500",
1829
2011
  error: "text-error-500",
@@ -1860,7 +2042,7 @@ var Spinner = ({
1860
2042
  className: chunkYERNSNT4_cjs.cn(
1861
2043
  "inline-block animate-spin rounded-full border-current border-t-transparent",
1862
2044
  sizeStyles4[size],
1863
- !textColor && colorStyles[color]
2045
+ !textColor && colorStyles2[color]
1864
2046
  ),
1865
2047
  style: textColor ? { color: textColor } : void 0
1866
2048
  }
@@ -2686,7 +2868,7 @@ var DatePicker = ({
2686
2868
  React5.useEffect(() => {
2687
2869
  const flatPickr = flatpickr__default.default(`#${id}`, {
2688
2870
  mode: mode || "single",
2689
- static: true,
2871
+ appendTo: document.body,
2690
2872
  monthSelectorType: "static",
2691
2873
  dateFormat: "Y-m-d",
2692
2874
  defaultDate,
@@ -2742,7 +2924,7 @@ var DateTimePicker = ({
2742
2924
  React5.useEffect(() => {
2743
2925
  const flatPickr = flatpickr__default.default(`#${id}-date`, {
2744
2926
  mode: "single",
2745
- static: true,
2927
+ appendTo: document.body,
2746
2928
  monthSelectorType: "static",
2747
2929
  dateFormat: "Y-m-d",
2748
2930
  defaultDate,
@@ -3004,6 +3186,7 @@ exports.Label = Label;
3004
3186
  exports.MultiSelect = MultiSelect;
3005
3187
  exports.Pagination = Pagination;
3006
3188
  exports.PasswordInput = PasswordInput;
3189
+ exports.PollResults = PollResults;
3007
3190
  exports.Popover = Popover;
3008
3191
  exports.Progress = Progress;
3009
3192
  exports.Radio = Radio;