@dynamic-framework/ui-react 2.7.0 → 2.8.0

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.
Files changed (40) hide show
  1. package/dist/css/dynamic-ui.css +272 -1
  2. package/dist/css/dynamic-ui.min.css +2 -2
  3. package/dist/index.esm.js +503 -267
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.js +507 -266
  6. package/dist/index.js.map +1 -1
  7. package/dist/types/components/DDropdown/DDropdown.d.ts +10 -2
  8. package/dist/types/components/DOtp/DOtp.d.ts +1 -0
  9. package/dist/types/components/DOtp/components/OtpCountdown.d.ts +3 -2
  10. package/dist/types/components/DVoucher/DVoucher.d.ts +3 -1
  11. package/dist/types/components/DVoucher/hooks/useScreenshotDownload.d.ts +1 -1
  12. package/dist/types/components/DVoucher/hooks/useScreenshotDownload.spec.d.ts +1 -0
  13. package/dist/types/components/DVoucher/hooks/useScreenshotWebShare.d.ts +1 -1
  14. package/dist/types/components/DVoucher/hooks/useScreenshotWebShare.spec.d.ts +1 -0
  15. package/dist/types/components/DVoucher/index.d.ts +3 -0
  16. package/dist/types/components/index.d.ts +1 -1
  17. package/dist/types/hooks/index.d.ts +3 -0
  18. package/dist/types/hooks/tests/useCountdown.spec.d.ts +1 -0
  19. package/dist/types/hooks/useCountdown.d.ts +14 -0
  20. package/dist/types/hooks/useInputCurrency.d.ts +1 -6
  21. package/dist/types/hooks/useOtp.d.ts +98 -0
  22. package/package.json +1 -1
  23. package/src/style/abstracts/variables/_+import.scss +1 -0
  24. package/src/style/abstracts/variables/_input-currency.scss +1 -0
  25. package/src/style/components/_+import.scss +7 -0
  26. package/src/style/components/_d-box-file.scss +5 -0
  27. package/src/style/components/_d-close.scss +1 -0
  28. package/src/style/components/_d-confirm-modal.scss +17 -0
  29. package/src/style/components/_d-data-state-wrapper.scss +47 -0
  30. package/src/style/components/_d-datepicker.scss +11 -0
  31. package/src/style/components/_d-error-boundary.scss +7 -0
  32. package/src/style/components/_d-input-currency.scss +5 -0
  33. package/src/style/components/_d-otp.scss +54 -0
  34. package/src/style/components/_d-password-strength-meter.scss +76 -0
  35. package/src/style/components/_d-popover.scss +4 -0
  36. package/src/style/components/_d-stepper.scss +8 -0
  37. package/src/style/components/_d-tabs.scss +18 -0
  38. package/src/style/components/_d-voucher.scss +30 -0
  39. package/dist/types/components/DOtp/DOtpspec.d.ts +0 -0
  40. package/dist/types/components/DOtp/hooks/useCountdown.d.ts +0 -4
package/dist/index.esm.js CHANGED
@@ -1106,7 +1106,7 @@ function DBoxFile(_a) {
1106
1106
  'd-box-file-invalid': isDragInvalid,
1107
1107
  }, className), style: style }, dataAttributes, { children: jsxs("div", Object.assign({ className: "d-box-file-dropzone", ref: rootRef, onDragEnter: handleDragEnter, onDragOver: (e) => e.preventDefault(), onDragLeave: handleDragLeave, onDrop: handleDrop, onClick: handleClick, onKeyDown: handleKeyDown }, (!props.disabled && !props.noKeyboard ? { tabIndex: 0 } : {}), { role: "presentation", children: [jsx("input", { type: "file", multiple: props.multiple, style: { display: 'none' }, ref: inputRef, disabled: props.disabled, onChange: handleFileSelect, onClick: (e) => e.stopPropagation(), tabIndex: -1, accept: acceptAttr }), icon && iconProp !== false && (jsx(DIcon, { icon: icon, familyClass: iconFamilyClass, familyPrefix: iconFamilyPrefix, materialStyle: iconMaterialStyle })), jsx("div", { className: "d-box-content", children: typeof children === 'function'
1108
1108
  ? children(openFileDialog)
1109
- : children || (jsx("p", { className: "text-center m-0", children: "Drag and drop some files here, or click to select files" })) })] })) })), !!files.length && (jsx("ul", { className: "d-box-files", children: files.map((file, index) => (jsx(ForwardedDInput, { value: file.name, iconStart: "Paperclip", iconEnd: "Trash", readOnly: true, onIconEndClick: () => handleRemoveFile(index) }, `${file.name} ${index}`))) }))] }));
1109
+ : children || (jsx("p", { className: "d-box-file-empty-text", children: "Drag and drop some files here, or click to select files" })) })] })) })), !!files.length && (jsx("ul", { className: "d-box-files", children: files.map((file, index) => (jsx(ForwardedDInput, { value: file.name, iconStart: "Paperclip", iconEnd: "Trash", readOnly: true, onIconEndClick: () => handleRemoveFile(index) }, `${file.name} ${index}`))) }))] }));
1110
1110
  }
1111
1111
 
1112
1112
  const DButton = forwardRef((props, ref) => {
@@ -1482,9 +1482,9 @@ function DDatePickerHeaderSelector({ date, changeYear, changeMonth, decreaseMont
1482
1482
  return (jsxs("div", { className: classNames('react-datepicker__header-selector react-datepicker__header-year-selector', className), style: style, children: [jsx(DButtonIcon, { icon: iconPrev || chevronLeft, size: iconSize, variant: "link", onClick: decreaseYear, disabled: prevYearButtonDisabled, "aria-label": prevYearAriaLabel, className: "header-button" }), jsx("p", { children: `${startYear} - ${endYear}` }), jsx(DButtonIcon, { icon: iconNext || chevronRight, size: iconSize, variant: "link", onClick: increaseYear, disabled: nextYearButtonDisabled, "aria-label": nextYearAriaLabel, className: "header-button" })] }));
1483
1483
  }
1484
1484
  if (pickerType === PickerType.Quarter || pickerType === PickerType.Month) {
1485
- return (jsxs("div", { className: classNames(`react-datepicker__header-selector react-datepicker__header-${pickerType}-selector`, className), style: style, children: [jsx(DButtonIcon, { icon: iconPrev || chevronLeft, size: iconSize, variant: "link", onClick: decreaseYear, disabled: prevMonthButtonDisabled, "aria-label": prevMonthAriaLabel, className: "header-button", style: { visibility: customHeaderCount === 0 ? 'visible' : 'hidden' } }), jsx("div", { className: "d-flex justify-content-center flex-grow-1", children: showHeaderSelectors ? (jsx(DSelect$1, { options: years, value: defaultYear, defaultValue: defaultYear, onChange: (year) => changeYear(Number(year === null || year === void 0 ? void 0 : year.value)), searchable: false })) : (jsx("p", { children: defaultYear === null || defaultYear === void 0 ? void 0 : defaultYear.label })) }), jsx(DButtonIcon, { icon: iconNext || chevronRight, size: iconSize, variant: "link", onClick: increaseYear, disabled: nextMonthButtonDisabled, "aria-label": nextMonthAriaLabel, className: "header-button", style: { visibility: customHeaderCount === monthsShown - 1 ? 'visible' : 'hidden' } })] }));
1485
+ return (jsxs("div", { className: classNames(`react-datepicker__header-selector react-datepicker__header-${pickerType}-selector`, className), style: style, children: [jsx(DButtonIcon, { icon: iconPrev || chevronLeft, size: iconSize, variant: "link", onClick: decreaseYear, disabled: prevMonthButtonDisabled, "aria-label": prevMonthAriaLabel, className: "header-button", style: { visibility: customHeaderCount === 0 ? 'visible' : 'hidden' } }), jsx("div", { className: "react-datepicker__header-year-selector", children: showHeaderSelectors ? (jsx(DSelect$1, { options: years, value: defaultYear, defaultValue: defaultYear, onChange: (year) => changeYear(Number(year === null || year === void 0 ? void 0 : year.value)), searchable: false })) : (jsx("p", { children: defaultYear === null || defaultYear === void 0 ? void 0 : defaultYear.label })) }), jsx(DButtonIcon, { icon: iconNext || chevronRight, size: iconSize, variant: "link", onClick: increaseYear, disabled: nextMonthButtonDisabled, "aria-label": nextMonthAriaLabel, className: "header-button", style: { visibility: customHeaderCount === monthsShown - 1 ? 'visible' : 'hidden' } })] }));
1486
1486
  }
1487
- return (jsxs(Fragment, { children: [jsxs("div", { className: "datepicker-top-header", children: [showHeaderSelectors && (jsx(DSelect$1, { options: years, value: defaultYear, defaultValue: defaultYear, onChange: (year) => changeYear(Number(year === null || year === void 0 ? void 0 : year.value)), searchable: false, className: "custom-year-selector" })), jsx("h4", { className: "mb-0 fw-normal", children: format(monthDate, formatHeaderDate, { locale }) })] }), jsxs("div", { className: classNames('react-datepicker__header-selector react-datepicker__header-day-selector', className), style: style, children: [jsx(DButtonIcon, { icon: iconPrev || chevronLeft, size: iconSize, variant: "link", onClick: decreaseMonth, disabled: prevMonthButtonDisabled, "aria-label": prevMonthAriaLabel, className: "header-button", style: { visibility: customHeaderCount === 0 ? 'visible' : 'hidden' } }), showHeaderSelectors ? (jsx(DSelect$1, { options: months, value: defaultMonth, defaultValue: defaultMonth, onChange: (month) => changeMonth((month === null || month === void 0 ? void 0 : month.value) || 0), searchable: false, className: "custom-month-selector" })) : (jsx("p", { children: `${defaultMonth.label} ${defaultYear === null || defaultYear === void 0 ? void 0 : defaultYear.label}` })), jsx(DButtonIcon, { icon: iconNext || chevronRight, size: iconSize, variant: "link", onClick: increaseMonth, disabled: nextMonthButtonDisabled, "aria-label": nextMonthAriaLabel, className: "header-button", style: { visibility: customHeaderCount === monthsShown - 1 ? 'visible' : 'hidden' } })] })] }));
1487
+ return (jsxs(Fragment, { children: [jsxs("div", { className: "datepicker-top-header", children: [showHeaderSelectors && (jsx(DSelect$1, { options: years, value: defaultYear, defaultValue: defaultYear, onChange: (year) => changeYear(Number(year === null || year === void 0 ? void 0 : year.value)), searchable: false, className: "custom-year-selector" })), jsx("h4", { className: "datepicker-top-header-title", children: format(monthDate, formatHeaderDate, { locale }) })] }), jsxs("div", { className: classNames('react-datepicker__header-selector react-datepicker__header-day-selector', className), style: style, children: [jsx(DButtonIcon, { icon: iconPrev || chevronLeft, size: iconSize, variant: "link", onClick: decreaseMonth, disabled: prevMonthButtonDisabled, "aria-label": prevMonthAriaLabel, className: "header-button", style: { visibility: customHeaderCount === 0 ? 'visible' : 'hidden' } }), showHeaderSelectors ? (jsx(DSelect$1, { options: months, value: defaultMonth, defaultValue: defaultMonth, onChange: (month) => changeMonth((month === null || month === void 0 ? void 0 : month.value) || 0), searchable: false, className: "custom-month-selector" })) : (jsx("p", { children: `${defaultMonth.label} ${defaultYear === null || defaultYear === void 0 ? void 0 : defaultYear.label}` })), jsx(DButtonIcon, { icon: iconNext || chevronRight, size: iconSize, variant: "link", onClick: increaseMonth, disabled: nextMonthButtonDisabled, "aria-label": nextMonthAriaLabel, className: "header-button", style: { visibility: customHeaderCount === monthsShown - 1 ? 'visible' : 'hidden' } })] })] }));
1488
1488
  }
1489
1489
 
1490
1490
  function DDatePicker(_a) {
@@ -1619,11 +1619,24 @@ function formatValue(value, currencyOptions) {
1619
1619
  }
1620
1620
  return currency(value, Object.assign(Object.assign({}, currencyOptions), { symbol: '' })).format();
1621
1621
  }
1622
- function useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref) {
1622
+ function useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref, minValue, maxValue) {
1623
1623
  const inputRef = useProvidedRefOrCreate(ref);
1624
+ const clampValue = useCallback((newValue) => {
1625
+ if (newValue === undefined) {
1626
+ return newValue;
1627
+ }
1628
+ let clampedValue = newValue;
1629
+ if (minValue !== undefined) {
1630
+ clampedValue = Math.max(clampedValue, minValue);
1631
+ }
1632
+ if (maxValue !== undefined) {
1633
+ clampedValue = Math.min(clampedValue, maxValue);
1634
+ }
1635
+ return clampedValue;
1636
+ }, [minValue, maxValue]);
1624
1637
  const [innerType, setInnerType] = useState('text');
1625
- const [innerNumber, setInnerNumber] = useState(value);
1626
- const [innerString, setInnerString] = useState(formatValue(value, currencyOptions));
1638
+ const [innerNumber, setInnerNumber] = useState(clampValue(value));
1639
+ const [innerString, setInnerString] = useState(formatValue(clampValue(value), currencyOptions));
1627
1640
  const handleOnFocus = useCallback((event) => {
1628
1641
  event.stopPropagation();
1629
1642
  setInnerType('number');
@@ -1632,15 +1645,14 @@ function useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref
1632
1645
  const handleOnBlur = useCallback((event) => {
1633
1646
  event.stopPropagation();
1634
1647
  setInnerType('text');
1648
+ const clampedNumber = clampValue(innerNumber);
1649
+ if (clampedNumber !== innerNumber) {
1650
+ setInnerNumber(clampedNumber);
1651
+ setInnerString(formatValue(clampedNumber, currencyOptions));
1652
+ onChange === null || onChange === void 0 ? void 0 : onChange(clampedNumber);
1653
+ }
1635
1654
  onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
1636
- }, [onBlur]);
1637
- const generateStyleVariables = useMemo(() => ({
1638
- [`--${PREFIX_BS}input-currency-component-symbol-color`]: `var(--${PREFIX_BS}secondary)`,
1639
- [`--${PREFIX_BS}input-currency-symbol-color`]: `var(--${PREFIX_BS}input-currency-component-symbol-color)`,
1640
- }), []);
1641
- const generateSymbolStyleVariables = useMemo(() => ({
1642
- color: `var(--${PREFIX_BS}input-currency-symbol-color)`,
1643
- }), []);
1655
+ }, [onBlur, innerNumber, clampValue, currencyOptions, onChange]);
1644
1656
  const handleOnChange = useCallback((newValue) => {
1645
1657
  const newNumber = (newValue === undefined || newValue === '') ? undefined : Number(newValue);
1646
1658
  if (newNumber !== innerNumber) {
@@ -1649,7 +1661,12 @@ function useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref
1649
1661
  onChange === null || onChange === void 0 ? void 0 : onChange(newNumber);
1650
1662
  }
1651
1663
  }, [currencyOptions, onChange, innerNumber]);
1664
+ const isMountedRef = useRef(false);
1652
1665
  useEffect(() => {
1666
+ if (!isMountedRef.current) {
1667
+ isMountedRef.current = true;
1668
+ return;
1669
+ }
1653
1670
  if (value !== innerNumber) {
1654
1671
  setInnerNumber(value);
1655
1672
  setInnerString(formatValue(value, currencyOptions));
@@ -1663,8 +1680,6 @@ function useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref
1663
1680
  handleOnFocus,
1664
1681
  handleOnChange,
1665
1682
  handleOnBlur,
1666
- generateStyleVariables,
1667
- generateSymbolStyleVariables,
1668
1683
  };
1669
1684
  }
1670
1685
 
@@ -1769,6 +1784,149 @@ function useConfirmModal(config) {
1769
1784
  return { open };
1770
1785
  }
1771
1786
 
1787
+ /**
1788
+ * Generic seconds-based countdown hook.
1789
+ *
1790
+ * Starts counting down from `seconds` to `0` and exposes `restartCountdown`
1791
+ * to reset and restart the timer (e.g. for "resend code in Xs" patterns,
1792
+ * session-expiry banners, or any other cooldown/throttle UI).
1793
+ *
1794
+ * @example
1795
+ * const { secondsLeft, restartCountdown } = useCountdown(30);
1796
+ */
1797
+ function useCountdown(seconds) {
1798
+ const [secondsLeft, setSecondsLeft] = useState(seconds);
1799
+ const [isActive, setIsActive] = useState(true);
1800
+ const resetCountdown = useCallback((newSeconds = seconds) => {
1801
+ setIsActive(false);
1802
+ setSecondsLeft(newSeconds);
1803
+ }, [seconds]);
1804
+ const restartCountdown = useCallback(() => {
1805
+ resetCountdown(seconds);
1806
+ setIsActive(true);
1807
+ }, [resetCountdown, seconds]);
1808
+ useEffect(() => {
1809
+ if (!isActive) {
1810
+ return () => { };
1811
+ }
1812
+ const interval = setInterval(() => {
1813
+ setSecondsLeft((prevSeconds) => {
1814
+ const newSeconds = prevSeconds - 1;
1815
+ if (newSeconds <= 0) {
1816
+ clearInterval(interval);
1817
+ setIsActive(false);
1818
+ return 0;
1819
+ }
1820
+ return newSeconds;
1821
+ });
1822
+ }, 1000);
1823
+ return () => clearInterval(interval);
1824
+ }, [isActive]);
1825
+ return { secondsLeft, restartCountdown };
1826
+ }
1827
+
1828
+ /**
1829
+ * Headless hook exposing the same OTP logic used internally by `DOtp`
1830
+ * (code state, length + correctness validation, submit action and resend
1831
+ * countdown), without any bundled markup/styles.
1832
+ *
1833
+ * Use this when the default `DOtp` component doesn't match your design and
1834
+ * you need to build a fully custom UI while reusing the same behavior.
1835
+ *
1836
+ * Internally composes `useCountdown` for the resend timer and re-exposes
1837
+ * `secondsLeft`/`restartCountdown`, so there is no need to call
1838
+ * `useCountdown` separately.
1839
+ *
1840
+ * `invalid` becomes `true` both when the OTP is shorter than `otpSize` and
1841
+ * when `action` throws/rejects (e.g. the backend rejected the entered code
1842
+ * as incorrect). In the latter case, the thrown value is exposed as `error`
1843
+ * so custom UIs can render an appropriate message (e.g. "Invalid code" vs.
1844
+ * "Enter all digits"). `invalid`/`error` are cleared automatically as soon
1845
+ * as the user edits the code via `setOtp`.
1846
+ *
1847
+ * @example
1848
+ * function CustomOtp() {
1849
+ * const {
1850
+ * otp,
1851
+ * setOtp,
1852
+ * invalid,
1853
+ * error,
1854
+ * submit,
1855
+ * isLoading,
1856
+ * secondsLeft,
1857
+ * restartCountdown,
1858
+ * } = useOtp({ action: async () => verifyOtp(otp), otpSize: 6, seconds: 15 });
1859
+ *
1860
+ * return (
1861
+ * <>
1862
+ * <MyCustomPinInput value={otp} onChange={setOtp} invalid={invalid} />
1863
+ * {invalid && <span>{error ? 'Invalid code' : 'Enter all digits'}</span>}
1864
+ * <button disabled={isLoading} onClick={() => { submit().catch(() => {}); }}>
1865
+ * {isLoading ? 'Submitting...' : 'Submit'}
1866
+ * </button>
1867
+ * <button disabled={secondsLeft > 0} onClick={restartCountdown}>
1868
+ * Resend {secondsLeft > 0 ? `(${secondsLeft}s)` : ''}
1869
+ * </button>
1870
+ * </>
1871
+ * );
1872
+ * }
1873
+ */
1874
+ function useOtp({ action, otpSize = 6, seconds = 15, }) {
1875
+ const [otp, setOtpValue] = useState('');
1876
+ const [invalid, setInvalid] = useState(false);
1877
+ const [error, setError] = useState(null);
1878
+ const [isLoading, setIsLoading] = useState(false);
1879
+ const { secondsLeft, restartCountdown } = useCountdown(seconds);
1880
+ const setOtp = useCallback((value) => {
1881
+ setOtpValue((prevOtp) => {
1882
+ // Guard against no-op calls (e.g. DInputPin re-notifying the same
1883
+ // value when its onChange reference changes) so we don't wipe out
1884
+ // an `invalid`/`error` state that was just set by submit().
1885
+ if (prevOtp === value) {
1886
+ return prevOtp;
1887
+ }
1888
+ setInvalid(false);
1889
+ setError(null);
1890
+ return value;
1891
+ });
1892
+ }, []);
1893
+ const submit = useCallback(async () => {
1894
+ if (otp.length < otpSize) {
1895
+ setInvalid(true);
1896
+ setError(null);
1897
+ return;
1898
+ }
1899
+ setIsLoading(true);
1900
+ try {
1901
+ await action();
1902
+ setInvalid(false);
1903
+ setError(null);
1904
+ }
1905
+ catch (err) {
1906
+ setInvalid(true);
1907
+ setError(err);
1908
+ throw err;
1909
+ }
1910
+ finally {
1911
+ setIsLoading(false);
1912
+ }
1913
+ }, [
1914
+ otp.length,
1915
+ action,
1916
+ otpSize,
1917
+ ]);
1918
+ return {
1919
+ otp,
1920
+ setOtp,
1921
+ invalid,
1922
+ error,
1923
+ submit,
1924
+ isLoading,
1925
+ secondsLeft,
1926
+ restartCountdown,
1927
+ };
1928
+ }
1929
+
1772
1930
  function DInputCounter(_a, ref) {
1773
1931
  var { minValue, maxValue, value = minValue, invalid, iconStart: iconStartProp, iconEnd: iconEndProp, iconStartAriaLabel = 'decrease action', iconEndAriaLabel = 'increase action', style, onChange } = _a, props = __rest(_a, ["minValue", "maxValue", "value", "invalid", "iconStart", "iconEnd", "iconStartAriaLabel", "iconEndAriaLabel", "style", "onChange"]);
1774
1932
  const { handleOnWheel, } = useDisableInputWheel(ref);
@@ -1811,8 +1969,8 @@ function DInputCurrency(_a, ref) {
1811
1969
  var { value, minValue, maxValue, currencyCode, onFocus, onBlur, onChange } = _a, props = __rest(_a, ["value", "minValue", "maxValue", "currencyCode", "onFocus", "onBlur", "onChange"]);
1812
1970
  const { currency: currencyOptions } = useDContext();
1813
1971
  const { handleOnWheel, } = useDisableInputWheel(ref);
1814
- const { inputRef, innerValue, innerType, handleOnFocus, handleOnChange, handleOnBlur, generateStyleVariables, generateSymbolStyleVariables, } = useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref);
1815
- return (jsx(ForwardedDInput, Object.assign({ ref: inputRef, value: innerValue, onChange: handleOnChange, style: generateStyleVariables, inputMode: "decimal", type: innerType, onFocus: handleOnFocus, onBlur: handleOnBlur, onWheel: handleOnWheel, inputStart: (jsx("span", { slot: "input-start", style: generateSymbolStyleVariables, children: currencyCode || currencyOptions.symbol })) }, props)));
1972
+ const { inputRef, innerValue, innerType, handleOnFocus, handleOnChange, handleOnBlur, } = useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref, minValue, maxValue);
1973
+ return (jsx(ForwardedDInput, Object.assign({ ref: inputRef, value: innerValue, onChange: handleOnChange, inputMode: "decimal", type: innerType, onFocus: handleOnFocus, onBlur: handleOnBlur, onWheel: handleOnWheel, inputStart: (jsx("span", { slot: "input-start", className: "d-input-currency-symbol", children: currencyCode || currencyOptions.symbol })) }, props)));
1816
1974
  }
1817
1975
  const ForwardedDInputCurrency = forwardRef(DInputCurrency);
1818
1976
  ForwardedDInputCurrency.displayName = 'DInputCurrency';
@@ -1834,26 +1992,25 @@ ForwardedDInputPassword.displayName = 'DInputPassword';
1834
1992
 
1835
1993
  function PasswordCheckItem({ password, regex, text, }) {
1836
1994
  const isValid = regex.test(password);
1837
- return (jsxs("li", { className: "d-flex gap-2 align-items-start small text-gray-600", children: [jsx(DIcon, { className: classNames('flex-shrink-0', isValid ? 'text-success' : 'text-gray-300'), icon: isValid ? 'CircleCheck' : 'Circle', size: "16px" }), jsx("span", { className: classNames({ 'text-success': isValid }), children: text })] }));
1995
+ return (jsxs("li", { className: `d-password-check-item${isValid ? ' is-valid' : ''}`, children: [jsx(DIcon, { className: "d-password-check-item-icon", icon: isValid ? 'CircleCheck' : 'Circle', size: "16px" }), jsx("span", { children: text })] }));
1838
1996
  }
1839
1997
 
1840
- const getColorClass = (strength, total) => {
1998
+ const getStrengthModifier = (strength, total) => {
1841
1999
  const percentage = total > 0 ? strength / total : 0;
1842
2000
  if (percentage === 0)
1843
- return 'bg-gray-200';
2001
+ return 'is-empty';
1844
2002
  if (percentage <= 0.25)
1845
- return 'bg-danger';
2003
+ return 'is-weak';
1846
2004
  if (percentage <= 0.5)
1847
- return 'bg-warning';
2005
+ return 'is-fair';
1848
2006
  if (percentage <= 0.75)
1849
- return 'bg-info';
1850
- return 'bg-success';
2007
+ return 'is-good';
2008
+ return 'is-strong';
1851
2009
  };
1852
2010
  function PasswordStrengthBar({ strength, total }) {
1853
2011
  const percentage = total > 0 ? (strength / total) * 100 : 0;
1854
- return (jsx("div", { className: "w-100 rounded-3 overflow-hidden bg-gray-100 mb-2", style: { height: '8px' }, children: jsx("div", { className: `h-100 ${getColorClass(strength, total)}`, style: {
2012
+ return (jsx("div", { className: "d-password-strength-bar", children: jsx("div", { className: `d-password-strength-bar-fill ${getStrengthModifier(strength, total)}`, style: {
1855
2013
  width: `${percentage}%`,
1856
- transition: 'width 0.3s ease-in-out',
1857
2014
  } }) }));
1858
2015
  }
1859
2016
 
@@ -1889,7 +2046,7 @@ function PasswordChecksList({ password, validationMessages, enabledChecks, }) {
1889
2046
  const passwordChecks = allChecks.filter((check) => enabledChecks.includes(check.key));
1890
2047
  const passed = passwordChecks.filter((r) => r.regex.test(password)).length;
1891
2048
  const total = passwordChecks.length;
1892
- return (jsxs("div", { className: "mt-2", children: [jsx(PasswordStrengthBar, { strength: passed, total: total }), jsx("ul", { className: "list-unstyled m-0 d-flex flex-column gap-2", children: passwordChecks.map(({ key, regex, text }) => (jsx(PasswordCheckItem, { password: password, regex: regex, text: text }, key))) })] }));
2049
+ return (jsxs("div", { className: "d-password-strength-meter", children: [jsx(PasswordStrengthBar, { strength: passed, total: total }), jsx("ul", { className: "d-password-check-list", children: passwordChecks.map(({ key, regex, text }) => (jsx(PasswordCheckItem, { password: password, regex: regex, text: text }, key))) })] }));
1893
2050
  }
1894
2051
 
1895
2052
  const DEFAULT_VALIDATION_MESSAGES = {
@@ -2239,7 +2396,7 @@ var DListGroup$1 = Object.assign(DListGroup, {
2239
2396
  function DModalHeader({ showCloseButton, onClose, children, className, style, iconFamilyClass, iconFamilyPrefix, icon: iconProp, materialStyle = false, }) {
2240
2397
  const { iconMap: { xLg, }, } = useDContext();
2241
2398
  const icon = useMemo(() => iconProp || xLg, [iconProp, xLg]);
2242
- return (jsxs(Fragment, { children: [jsxs("div", { className: classNames('modal-header', className), style: style, children: [jsx("div", { children: children }), showCloseButton && (jsx("button", { type: "button", className: "d-close align-self-center", "aria-label": "Close", onClick: onClose, children: jsx(DIcon, { icon: icon, familyClass: iconFamilyClass, familyPrefix: iconFamilyPrefix, materialStyle: materialStyle }) }))] }), jsx("div", { className: "d-modal-separator" })] }));
2399
+ return (jsxs(Fragment, { children: [jsxs("div", { className: classNames('modal-header', className), style: style, children: [jsx("div", { children: children }), showCloseButton && (jsx("button", { type: "button", className: "d-close", "aria-label": "Close", onClick: onClose, children: jsx(DIcon, { icon: icon, familyClass: iconFamilyClass, familyPrefix: iconFamilyPrefix, materialStyle: materialStyle }) }))] }), jsx("div", { className: "d-modal-separator" })] }));
2243
2400
  }
2244
2401
 
2245
2402
  function DModalBody({ children, className, style, }) {
@@ -2283,7 +2440,7 @@ var DModal$1 = Object.assign(DModal, {
2283
2440
  function DOffcanvasHeader({ showCloseButton, onClose, children, className, style, iconFamilyClass, iconFamilyPrefix, icon: iconProp, materialStyle = false, }) {
2284
2441
  const { iconMap: { xLg, }, } = useDContext();
2285
2442
  const icon = useMemo(() => iconProp || xLg, [iconProp, xLg]);
2286
- return (jsxs(Fragment, { children: [jsxs("div", { className: classNames('offcanvas-header', className), style: style, children: [jsx("div", { children: children }), showCloseButton && (jsx("button", { type: "button", className: "d-close align-self-center", "aria-label": "Close", onClick: onClose, children: jsx(DIcon, { icon: icon, familyClass: iconFamilyClass, familyPrefix: iconFamilyPrefix, materialStyle: materialStyle }) }))] }), jsx("div", { className: "d-offcanvas-separator" })] }));
2443
+ return (jsxs(Fragment, { children: [jsxs("div", { className: classNames('offcanvas-header', className), style: style, children: [jsx("div", { children: children }), showCloseButton && (jsx("button", { type: "button", className: "d-close", "aria-label": "Close", onClick: onClose, children: jsx(DIcon, { icon: icon, familyClass: iconFamilyClass, familyPrefix: iconFamilyPrefix, materialStyle: materialStyle }) }))] }), jsx("div", { className: "d-offcanvas-separator" })] }));
2287
2444
  }
2288
2445
 
2289
2446
  function DOffcanvasBody({ children, className, style, }) {
@@ -2410,7 +2567,7 @@ function DPopover({ children, renderComponent, open, setOpen, adjustContentToRen
2410
2567
  }
2411
2568
  const reference = cloneElement(triggerElement, Object.assign({ ref: refs.setReference }, getReferenceProps(triggerElement.props)));
2412
2569
  return (jsxs("div", Object.assign({ className: classNames('d-popover', className), style: generateStyleVariables }, dataAttributes, { children: [reference, isOpen && (jsx(FloatingFocusManager, { context: context, modal: false, children: jsx("div", Object.assign({ className: classNames('d-popover-content', {
2413
- 'w-100': adjustContentToRender,
2570
+ 'd-popover-content-fill': adjustContentToRender,
2414
2571
  }), ref: refs.setFloating, style: floatingStyles, "aria-labelledby": headingId }, getFloatingProps(), { children: children })) }))] })));
2415
2572
  }
2416
2573
 
@@ -2474,13 +2631,13 @@ function DStepper$1({ options, currentStep, className, style, }) {
2474
2631
  }
2475
2632
 
2476
2633
  function DStepper({ options, currentStep, iconSuccess, iconSuccessFamilyClass, iconSuccessFamilyPrefix, iconSuccessMaterialStyle = false, vertical = false, breakpoint = 'lg', className, completed = false, style, dataAttributes, }) {
2477
- return (jsxs("div", Object.assign({ className: className, style: style }, dataAttributes, { children: [jsx("div", { className: `d-block d-${breakpoint}-none`, children: jsx(DStepper$1, { options: options, currentStep: currentStep }) }), jsx("div", { className: `d-none d-${breakpoint}-block`, children: jsx(DStepper$2, { options: options, currentStep: currentStep, vertical: vertical, iconSuccess: iconSuccess, iconSuccessFamilyClass: iconSuccessFamilyClass, iconSuccessFamilyPrefix: iconSuccessFamilyPrefix, iconSuccessMaterialStyle: iconSuccessMaterialStyle, completed: completed }) })] })));
2634
+ return (jsxs("div", Object.assign({ className: className, style: style }, dataAttributes, { children: [jsx("div", { className: classNames('d-stepper-mobile', `d-block d-${breakpoint}-none`), children: jsx(DStepper$1, { options: options, currentStep: currentStep }) }), jsx("div", { className: classNames('d-stepper-desktop', `d-none d-${breakpoint}-block`), children: jsx(DStepper$2, { options: options, currentStep: currentStep, vertical: vertical, iconSuccess: iconSuccess, iconSuccessFamilyClass: iconSuccessFamilyClass, iconSuccessFamilyPrefix: iconSuccessFamilyPrefix, iconSuccessMaterialStyle: iconSuccessMaterialStyle, completed: completed }) })] })));
2478
2635
  }
2479
2636
 
2480
2637
  const ARROW_WIDTH = 8;
2481
2638
  const ARROW_HEIGHT = 4;
2482
- const GAP = 2;
2483
- function DTooltip({ className, childrenClassName, style, offSet = ARROW_HEIGHT + GAP, padding, withFocus = false, withClick = false, withHover = true, open = false, placement = 'top', size, Component, children, }) {
2639
+ const GAP$1 = 2;
2640
+ function DTooltip({ className, childrenClassName, style, offSet = ARROW_HEIGHT + GAP$1, padding, withFocus = false, withClick = false, withHover = true, open = false, placement = 'top', size, Component, children, }) {
2484
2641
  const [isOpen, setIsOpen] = useState(open);
2485
2642
  const arrowRef = useRef(null);
2486
2643
  const { refs, context, floatingStyles, } = useFloating({
@@ -2549,8 +2706,16 @@ function DTabs({ children, defaultSelected, onChange, options, className, classN
2549
2706
  useEffect(() => {
2550
2707
  setSelected(defaultSelected);
2551
2708
  }, [defaultSelected]);
2552
- const generateClasses = useMemo(() => (Object.assign({ nav: true, 'flex-column align-items-center': vertical && variant !== 'tabs', [`nav-${variant}`]: true }, className && { [className]: true })), [vertical, variant, className]);
2709
+ const generateClasses = useMemo(() => (Object.assign({ nav: true, 'd-tabs-nav-vertical': vertical && variant !== 'tabs', [`nav-${variant}`]: true }, className && { [className]: true })), [vertical, variant, className]);
2553
2710
  const tabRefs = useRef([]);
2711
+ // Always holds the latest `options` without needing to be a dependency:
2712
+ // `options` is commonly passed as an inline array literal (e.g.
2713
+ // `options={[{ label: 'SMS', tab: 'sms' }, ...]}`), so it's a new array
2714
+ // reference on every parent render even when its content hasn't changed.
2715
+ // Reading it from this ref (updated synchronously on every render) lets
2716
+ // the focus effect below react only to `selected` changing.
2717
+ const optionsRef = useRef(options);
2718
+ optionsRef.current = options;
2554
2719
  useEffect(() => {
2555
2720
  tabRefs.current = options.map((_, i) => tabRefs.current[i] || createRef());
2556
2721
  }, [options]);
@@ -2565,20 +2730,28 @@ function DTabs({ children, defaultSelected, onChange, options, className, classN
2565
2730
  setSelected(firstEnabled.tab);
2566
2731
  }
2567
2732
  }, [options, selected]);
2568
- // Declarative focus management
2569
- const focusTab = (idx) => {
2733
+ // Declarative focus management. Wrapped in `useCallback` with an empty
2734
+ // dependency array since it only reads from the `tabRefs` ref, so its
2735
+ // identity stays stable across renders and it can safely be used as an
2736
+ // effect dependency below.
2737
+ const focusTab = useCallback((idx) => {
2570
2738
  var _a;
2571
2739
  if ((_a = tabRefs.current[idx]) === null || _a === void 0 ? void 0 : _a.current) {
2572
2740
  tabRefs.current[idx].current.focus();
2573
2741
  }
2574
- };
2575
- // Focus selected tab when selected changes
2742
+ }, []);
2743
+ // Focus selected tab when selected changes.
2744
+ // Reads `options` from `optionsRef` (see comment above) instead of
2745
+ // depending on `options` directly, so a parent re-render that merely
2746
+ // creates a new `options` reference with identical content doesn't call
2747
+ // `focusTab` again and steal focus away from unrelated elements on the
2748
+ // page (e.g. an OTP input).
2576
2749
  useEffect(() => {
2577
- const idx = options.findIndex((opt) => opt.tab === selected && !opt.disabled);
2750
+ const idx = optionsRef.current.findIndex((opt) => opt.tab === selected && !opt.disabled);
2578
2751
  if (idx !== -1) {
2579
2752
  focusTab(idx);
2580
2753
  }
2581
- }, [selected, options]);
2754
+ }, [selected, focusTab]);
2582
2755
  const handleKeyDown = useCallback((idx, e) => {
2583
2756
  const count = options.length;
2584
2757
  if (count === 0)
@@ -2607,7 +2780,7 @@ function DTabs({ children, defaultSelected, onChange, options, className, classN
2607
2780
  }
2608
2781
  }
2609
2782
  }
2610
- }, [options, vertical]);
2783
+ }, [options, vertical, focusTab]);
2611
2784
  let tablistProps = {};
2612
2785
  if (ariaLabelledBy) {
2613
2786
  tablistProps = { 'aria-labelledby': ariaLabelledBy };
@@ -2620,12 +2793,12 @@ function DTabs({ children, defaultSelected, onChange, options, className, classN
2620
2793
  isSelected,
2621
2794
  }), [isSelected]);
2622
2795
  return (jsx(TabContext.Provider, { value: value, children: jsxs("div", Object.assign({ className: classNames({
2623
- 'd-flex w-100': true,
2624
- 'flex-column': !vertical || variant === 'tabs',
2796
+ 'd-tabs': true,
2797
+ 'd-tabs-column': !vertical || variant === 'tabs',
2625
2798
  }), style: style }, dataAttributes, { children: [jsx("ul", Object.assign({ className: classNames(generateClasses), role: "tablist", "aria-orientation": vertical ? 'vertical' : undefined }, tablistProps, { children: options.map((option, idx) => {
2626
2799
  const isTabSelected = !!option.tab && option.tab === selected;
2627
2800
  return (jsx("li", { role: "presentation", className: "nav-item", children: jsx("button", { ref: tabRefs.current[idx], id: `${option.tab}Tab`, className: classNames('nav-link', { active: isTabSelected }, classNameTab), type: "button", role: "tab", "aria-controls": `${option.tab}Pane`, "aria-selected": isTabSelected, tabIndex: isTabSelected ? 0 : -1, disabled: option.disabled, onClick: () => onSelect(option), onKeyDown: (e) => handleKeyDown(idx, e), children: option.label }) }, option.tab));
2628
- }) })), jsx("div", { className: "tab-content w-100", children: children })] })) }));
2801
+ }) })), jsx("div", { className: "d-tabs-content tab-content", children: children })] })) }));
2629
2802
  }
2630
2803
  var DTabs$1 = Object.assign(DTabs, {
2631
2804
  Tab: DTabContent,
@@ -2687,11 +2860,11 @@ function useDToast() {
2687
2860
  if (!description) {
2688
2861
  return (jsx(DToast$1, { className: classNames({
2689
2862
  [`toast-${color}`]: !!color,
2690
- }, 'show'), children: jsxs(DToast$1.Body, { children: [icon && (jsx(DIcon, { className: "toast-icon", icon: icon })), jsx("p", { className: "toast-title", children: title }), jsx("button", { type: "button", className: "d-close align-self-center", "aria-label": "Close", onClick: () => toast.dismiss(id), children: jsx(DIcon, { icon: closeIcon || xLg }) })] }) }));
2863
+ }, 'show'), children: jsxs(DToast$1.Body, { children: [icon && (jsx(DIcon, { className: "toast-icon", icon: icon })), jsx("p", { className: "toast-title", children: title }), jsx("button", { type: "button", className: "d-close", "aria-label": "Close", onClick: () => toast.dismiss(id), children: jsx(DIcon, { icon: closeIcon || xLg }) })] }) }));
2691
2864
  }
2692
2865
  return (jsxs(DToast$1, { className: classNames({
2693
2866
  [`toast-${color}`]: !!color,
2694
- }, 'show'), children: [jsxs(DToast$1.Header, { children: [icon && (jsx(DIcon, { className: "toast-icon", icon: icon })), jsx("p", { className: "toast-title", children: title }), timestamp && (jsx("small", { className: "toast-timestamp", children: timestamp })), jsx("button", { type: "button", className: "d-close align-self-center", "aria-label": "Close", onClick: () => toast.dismiss(id), children: jsx(DIcon, { icon: closeIcon || xLg }) })] }), jsx(DToast$1.Body, { children: jsx("span", { children: description }) })] }));
2867
+ }, 'show'), children: [jsxs(DToast$1.Header, { children: [icon && (jsx(DIcon, { className: "toast-icon", icon: icon })), jsx("p", { className: "toast-title", children: title }), timestamp && (jsx("small", { className: "toast-timestamp", children: timestamp })), jsx("button", { type: "button", className: "d-close", "aria-label": "Close", onClick: () => toast.dismiss(id), children: jsx(DIcon, { icon: closeIcon || xLg }) })] }), jsx(DToast$1.Body, { children: jsx("span", { children: description }) })] }));
2695
2868
  }, toastProps);
2696
2869
  }, [xLg]);
2697
2870
  return {
@@ -2713,11 +2886,10 @@ async function configureI8n(resources, _a = {}) {
2713
2886
  .then((t) => t);
2714
2887
  }
2715
2888
 
2716
- function getQueryString(value, config = {
2717
- useSearch: true,
2718
- }) {
2719
- const urlParams = new URLSearchParams(config.useSearch ? window.location.search : '');
2720
- return urlParams.get(value) || config.default;
2889
+ function getQueryString(value, config = {}) {
2890
+ const { useSearch = true, default: defaultValue } = config;
2891
+ const urlParams = new URLSearchParams(useSearch ? window.location.search : '');
2892
+ return urlParams.get(value) || defaultValue;
2721
2893
  }
2722
2894
 
2723
2895
  function changeQueryString(values, { useSearch = true, pushState = false, } = {}) {
@@ -2840,89 +3012,228 @@ function DCreditCard({ brand = 'visa', name, number, holderText = 'Card Holder',
2840
3012
  return (jsxs("div", { className: classNames('d-credit-card', isVertical && 'is-vertical', className), children: [jsxs("div", { className: "d-credit-card-header", children: [jsx("img", { src: logoImage || BRAND_LOGOS[brand] || DEFAULT_IMAGE, alt: brand, className: "d-credit-card-logo", width: 100 }), isChipVisible && (jsx("div", { className: "d-credit-card-chip", children: jsx("img", { src: CHIP_IMAGE, alt: "chip", width: 30, className: "d-credit-card-chip-image" }) }))] }), jsxs("div", { className: "d-credit-card-details", children: [jsx("div", { className: "d-credit-card-number", children: number }), jsx("small", { className: "d-credit-card-holder-text", children: holderText }), jsx("span", { className: "d-credit-card-name", children: name })] })] }));
2841
3013
  }
2842
3014
 
3015
+ // Gap between the toggle and the menu.
3016
+ const GAP = 4;
3017
+ // Minimum distance the menu should keep from the viewport edges.
3018
+ const VIEWPORT_PADDING = 8;
3019
+ // Minimum usable height left for the menu before we consider a side unusable.
3020
+ const MIN_USABLE_SPACE = 120;
2843
3021
  const getItemClass = (action) => classNames({
2844
3022
  'dropdown-item d-flex align-items-center': true,
2845
3023
  [`dropdown-item-${action.color}`]: !!action.color,
2846
3024
  disabled: action.disabled,
2847
3025
  });
2848
- function DDropdown({ actions, dropdownToggle, className, classNameMenu, asPortal = false, placement = 'auto', }) {
3026
+ const clamp = (value, min, max) => {
3027
+ if (max < min)
3028
+ return min;
3029
+ return Math.min(Math.max(value, min), max);
3030
+ };
3031
+ /**
3032
+ * `position: fixed` is normally relative to the viewport, but any ancestor
3033
+ * with a `transform`, `perspective`, `filter`, `will-change` (of those) or a
3034
+ * non-trivial `contain` establishes a new containing block, making `fixed`
3035
+ * behave like `absolute` relative to that ancestor instead (e.g. Storybook's
3036
+ * docs canvas wrapper, animated cards, sticky containers, etc.). We detect
3037
+ * that ancestor so the computed coordinates can be adjusted accordingly,
3038
+ * keeping the menu correctly anchored to the toggle in every context.
3039
+ */
3040
+ const getFixedPositioningOffset = (fromEl) => {
3041
+ var _a;
3042
+ let node = (_a = fromEl === null || fromEl === void 0 ? void 0 : fromEl.parentElement) !== null && _a !== void 0 ? _a : null;
3043
+ while (node && node !== document.body) {
3044
+ const cs = window.getComputedStyle(node);
3045
+ // Treat empty/undefined values as "unset" (jsdom returns "" rather than
3046
+ // the CSS initial value like "none" for some of these properties).
3047
+ const isSet = (value) => !!value && value !== 'none';
3048
+ const establishesContainingBlock = isSet(cs.transform)
3049
+ || isSet(cs.perspective)
3050
+ || isSet(cs.filter)
3051
+ || isSet(cs.backdropFilter)
3052
+ || ['transform', 'perspective', 'filter'].some((prop) => { var _a; return (_a = cs.willChange) === null || _a === void 0 ? void 0 : _a.includes(prop); })
3053
+ || cs.contain.split(' ').some((value) => ['layout', 'paint', 'strict', 'content'].includes(value));
3054
+ if (establishesContainingBlock) {
3055
+ const rect = node.getBoundingClientRect();
3056
+ return { top: rect.top, left: rect.left };
3057
+ }
3058
+ node = node.parentElement;
3059
+ }
3060
+ return { top: 0, left: 0 };
3061
+ };
3062
+ /**
3063
+ * Resolves where the menu should render (flip) and where it should sit along
3064
+ * the cross axis (shift), so it always stays fully inside the viewport
3065
+ * regardless of where the toggle is placed or how the viewport is resized.
3066
+ * This mimics floating-ui's `flip` + `shift` middlewares without adding the
3067
+ * dependency, keeping the existing Bootstrap markup/classes.
3068
+ */
3069
+ const computeCoords = (toggleRect, menuSize, placement, alignment) => {
3070
+ const viewportWidth = window.innerWidth;
3071
+ const viewportHeight = window.innerHeight;
3072
+ const spaceBelow = viewportHeight - toggleRect.bottom;
3073
+ const spaceAbove = toggleRect.top;
3074
+ const spaceLeft = toggleRect.left;
3075
+ const spaceRight = viewportWidth - toggleRect.right;
3076
+ let side = placement === 'auto'
3077
+ ? 'down'
3078
+ : placement;
3079
+ if (placement === 'auto') {
3080
+ side = spaceBelow >= menuSize.height + GAP || spaceBelow >= spaceAbove ? 'down' : 'up';
3081
+ }
3082
+ // Flip within the current axis if the chosen side can't fit the menu but
3083
+ // the opposite side (same axis) can.
3084
+ if (side === 'down' && spaceBelow < menuSize.height + GAP && spaceAbove > spaceBelow) {
3085
+ side = 'up';
3086
+ }
3087
+ else if (side === 'up' && spaceAbove < menuSize.height + GAP && spaceBelow > spaceAbove) {
3088
+ side = 'down';
3089
+ }
3090
+ else if (side === 'start' && spaceLeft < menuSize.width + GAP && spaceRight > spaceLeft) {
3091
+ side = 'end';
3092
+ }
3093
+ else if (side === 'end' && spaceRight < menuSize.width + GAP && spaceLeft > spaceRight) {
3094
+ side = 'start';
3095
+ }
3096
+ // Cross-axis fallback: e.g. a horizontally-centered toggle on a narrow
3097
+ // viewport can't open sideways (neither `start` nor `end` fit), but there
3098
+ // is plenty of room above/below — switch the whole axis in that case
3099
+ // (and vice versa for `down`/`up` squeezed against a short viewport).
3100
+ const bestVerticalSpace = Math.max(spaceBelow, spaceAbove);
3101
+ const bestHorizontalSpace = Math.max(spaceLeft, spaceRight);
3102
+ if (side === 'down' || side === 'up') {
3103
+ if (bestVerticalSpace < MIN_USABLE_SPACE && bestHorizontalSpace > bestVerticalSpace) {
3104
+ side = spaceRight >= spaceLeft ? 'end' : 'start';
3105
+ }
3106
+ }
3107
+ else if (bestHorizontalSpace < menuSize.width + GAP
3108
+ && bestVerticalSpace > bestHorizontalSpace) {
3109
+ side = spaceBelow >= spaceAbove ? 'down' : 'up';
3110
+ }
3111
+ if (side === 'down' || side === 'up') {
3112
+ const top = side === 'down'
3113
+ ? toggleRect.bottom + GAP
3114
+ : toggleRect.top - menuSize.height - GAP;
3115
+ // Align horizontally per the requested `alignment` (start/end/center), then
3116
+ // fall back to the opposite edge if the preferred alignment would overflow
3117
+ // the viewport, and clamp as a final safety net so it never overflows.
3118
+ let left;
3119
+ if (alignment === 'end') {
3120
+ left = toggleRect.right - menuSize.width;
3121
+ }
3122
+ else if (alignment === 'center') {
3123
+ left = toggleRect.left + (toggleRect.width / 2) - (menuSize.width / 2);
3124
+ }
3125
+ else {
3126
+ left = toggleRect.left;
3127
+ }
3128
+ if (left + menuSize.width > viewportWidth - VIEWPORT_PADDING && alignment !== 'end') {
3129
+ const altLeft = toggleRect.right - menuSize.width;
3130
+ if (altLeft >= VIEWPORT_PADDING)
3131
+ left = altLeft;
3132
+ }
3133
+ else if (left < VIEWPORT_PADDING && alignment !== 'start') {
3134
+ const altLeft = toggleRect.left;
3135
+ if (altLeft + menuSize.width <= viewportWidth - VIEWPORT_PADDING)
3136
+ left = altLeft;
3137
+ }
3138
+ left = clamp(left, VIEWPORT_PADDING, Math.max(viewportWidth - menuSize.width - VIEWPORT_PADDING, VIEWPORT_PADDING));
3139
+ const availableHeight = side === 'down' ? spaceBelow : spaceAbove;
3140
+ // Cap strictly to the space actually available on the resolved side —
3141
+ // MIN_USABLE_SPACE is only meant to influence the flip/shift decision
3142
+ // above, not to force a taller box than what physically fits and
3143
+ // overflow the viewport.
3144
+ const maxUsableHeight = Math.max(availableHeight - GAP - VIEWPORT_PADDING, 0);
3145
+ return {
3146
+ top,
3147
+ left,
3148
+ minWidth: Math.max(toggleRect.width, 160),
3149
+ maxHeight: `${maxUsableHeight}px`,
3150
+ resolvedSide: side,
3151
+ };
3152
+ }
3153
+ // side is 'start' or 'end'
3154
+ let left = side === 'start'
3155
+ ? toggleRect.left - menuSize.width - GAP
3156
+ : toggleRect.right + GAP;
3157
+ // Safety-net clamp: in rare cases where the toggle is squeezed so tightly
3158
+ // that no side of any axis has enough room, keep the menu inside the
3159
+ // viewport rather than letting it render off-screen.
3160
+ left = clamp(left, VIEWPORT_PADDING, Math.max(viewportWidth - menuSize.width - VIEWPORT_PADDING, VIEWPORT_PADDING));
3161
+ // Shift vertically: align with the toggle's top edge by default, but switch
3162
+ // to the bottom edge (or clamp) if that would overflow the viewport.
3163
+ let { top } = toggleRect;
3164
+ if (top + menuSize.height > viewportHeight - VIEWPORT_PADDING) {
3165
+ top = toggleRect.bottom - menuSize.height;
3166
+ }
3167
+ top = clamp(top, VIEWPORT_PADDING, Math.max(viewportHeight - menuSize.height - VIEWPORT_PADDING, VIEWPORT_PADDING));
3168
+ return {
3169
+ top,
3170
+ left,
3171
+ minWidth: Math.max(toggleRect.width, 160),
3172
+ maxHeight: `${Math.max(viewportHeight - top - VIEWPORT_PADDING, 0)}px`,
3173
+ resolvedSide: side,
3174
+ };
3175
+ };
3176
+ function DDropdown({ actions, dropdownToggle, className, classNameMenu, asPortal = false, placement = 'auto', alignment = 'start', }) {
2849
3177
  const [open, setOpen] = useState(false);
2850
- const [position, setPosition] = useState('down');
2851
- const [menuCoords, setMenuCoords] = useState(null);
2852
- // Used in default (non-portal) mode: wraps the entire dropdown
2853
- const dropdownRef = useRef(null);
2854
- // Used in portal mode: ref on the toggle wrapper to read its viewport coords
3178
+ const [coords, setCoords] = useState(null);
3179
+ // Initialize from `placement` (falling back to 'down' for 'auto') so the
3180
+ // wrapper's `drop-*` class reflects the requested side even before the
3181
+ // first open/measurement, matching the previous `drop-${placement}` class
3182
+ // behavior (relevant for Bootstrap caret/orientation styling).
3183
+ const [resolvedSide, setResolvedSide] = useState(placement === 'auto' ? 'down' : placement);
3184
+ // Wraps the toggle in both modes — used to read its viewport-relative rect.
2855
3185
  const toggleRef = useRef(null);
2856
- // Ref on the rendered <ul> — used for outside-click detection in portal mode
3186
+ // Ref on the rendered <ul> — used to measure its size and for outside-click detection.
2857
3187
  const menuRef = useRef(null);
2858
3188
  const toggle = () => {
2859
- setMenuCoords(null);
3189
+ setCoords(null);
2860
3190
  setOpen((prev) => !prev);
2861
3191
  };
2862
- const resolvedInlinePosition = placement === 'auto' ? position : placement;
2863
- // Compute fixed coords from the toggle's bounding rect (portal mode only)
2864
- const computePortalCoords = useCallback(() => {
2865
- if (!toggleRef.current)
3192
+ // Measures the toggle + menu and (re)computes fixed, pixel-perfect coords
3193
+ // with collision detection, so the menu never overflows the viewport.
3194
+ const updatePosition = useCallback(() => {
3195
+ if (!toggleRef.current || !menuRef.current)
3196
+ return;
3197
+ const toggleRect = toggleRef.current.getBoundingClientRect();
3198
+ // If the toggle itself has scrolled completely out of the viewport
3199
+ // (regardless of `placement`), close the menu instead of leaving it
3200
+ // rendered detached from the toggle that opened it.
3201
+ const toggleIsMeasured = toggleRect.width > 0 || toggleRect.height > 0;
3202
+ const toggleIsOutOfView = toggleRect.bottom <= 0
3203
+ || toggleRect.top >= window.innerHeight
3204
+ || toggleRect.right <= 0
3205
+ || toggleRect.left >= window.innerWidth;
3206
+ if (toggleIsMeasured && toggleIsOutOfView) {
3207
+ setOpen(false);
2866
3208
  return;
2867
- const rect = toggleRef.current.getBoundingClientRect();
2868
- const spaceRight = window.innerWidth - rect.right;
2869
- const spaceBottom = window.innerHeight - rect.bottom;
2870
- const spaceTop = rect.top;
2871
- let resolvedPlacement;
2872
- if (placement === 'auto') {
2873
- if (spaceBottom < 150) {
2874
- resolvedPlacement = 'up';
2875
- }
2876
- else if (spaceRight < 150) {
2877
- resolvedPlacement = 'start';
2878
- }
2879
- else {
2880
- resolvedPlacement = 'down';
2881
- }
2882
- }
2883
- else {
2884
- resolvedPlacement = placement;
2885
- }
2886
- const openStart = resolvedPlacement === 'start';
2887
- const openEnd = resolvedPlacement === 'end';
2888
- const openUp = resolvedPlacement === 'up';
2889
- let availableHeight = spaceBottom;
2890
- if (openUp) {
2891
- availableHeight = spaceTop;
2892
- }
2893
- else if (openStart || openEnd) {
2894
- availableHeight = window.innerHeight - rect.top;
2895
- }
2896
- const verticalCoords = openUp
2897
- ? { bottom: window.innerHeight - rect.top + 4 }
2898
- : { top: openStart || openEnd ? rect.top : rect.bottom + 4 };
2899
- let horizontalCoords;
2900
- if (openStart) {
2901
- horizontalCoords = { right: window.innerWidth - rect.left + 4 };
2902
- }
2903
- else if (openEnd) {
2904
- horizontalCoords = { left: rect.right + 4 };
2905
- }
2906
- else {
2907
- horizontalCoords = { left: rect.left };
2908
3209
  }
2909
- setMenuCoords(Object.assign(Object.assign(Object.assign({}, verticalCoords), horizontalCoords), { minWidth: Math.max(rect.width, 160), maxHeight: `${Math.max(availableHeight - 12, 120)}px` }));
2910
- }, [placement]);
2911
- // Outside-click: default mode
2912
- useEffect(() => {
2913
- if (asPortal)
2914
- return () => { };
2915
- const handler = (event) => {
2916
- if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
2917
- setOpen(false);
2918
- }
2919
- };
2920
- document.addEventListener('mousedown', handler);
2921
- return () => document.removeEventListener('mousedown', handler);
2922
- }, [asPortal]);
2923
- // Outside-click: portal mode (must check both toggle and floating menu)
2924
- useEffect(() => {
2925
- if (!asPortal || !open)
3210
+ const menuRect = menuRef.current.getBoundingClientRect();
3211
+ const _a = computeCoords(toggleRect, { width: menuRect.width, height: menuRect.height }, placement, alignment), { resolvedSide: side } = _a, menuCoords = __rest(_a, ["resolvedSide"]);
3212
+ // In portal mode the menu is appended to document.body, whose containing
3213
+ // block for `fixed` is always the viewport (no adjustment needed). In
3214
+ // inline mode the menu stays next to the toggle, so we must look for a
3215
+ // containing-block-establishing ancestor starting from the toggle.
3216
+ const offset = asPortal
3217
+ ? { top: 0, left: 0 }
3218
+ : getFixedPositioningOffset(toggleRef.current);
3219
+ setResolvedSide(side);
3220
+ setCoords(Object.assign(Object.assign({}, menuCoords), { top: menuCoords.top - offset.top, left: menuCoords.left - offset.left }));
3221
+ }, [placement, alignment, asPortal]);
3222
+ // Throttles scroll/resize-triggered updates to at most once per frame,
3223
+ // avoiding repeated forced reflows on busy/high-frequency scroll events.
3224
+ const rafIdRef = useRef(null);
3225
+ const scheduleUpdatePosition = useCallback(() => {
3226
+ if (rafIdRef.current !== null)
3227
+ return;
3228
+ rafIdRef.current = window.requestAnimationFrame(() => {
3229
+ rafIdRef.current = null;
3230
+ updatePosition();
3231
+ });
3232
+ }, [updatePosition]);
3233
+ // Outside-click detection (checks both the toggle and the floating menu,
3234
+ // since in portal mode the menu isn't a DOM descendant of the toggle).
3235
+ useLayoutEffect(() => {
3236
+ if (!open)
2926
3237
  return () => { };
2927
3238
  const handler = (event) => {
2928
3239
  var _a, _b;
@@ -2933,35 +3244,25 @@ function DDropdown({ actions, dropdownToggle, className, classNameMenu, asPortal
2933
3244
  };
2934
3245
  document.addEventListener('mousedown', handler);
2935
3246
  return () => document.removeEventListener('mousedown', handler);
2936
- }, [asPortal, open]);
2937
- // Auto-position: default mode
2938
- useEffect(() => {
2939
- if (asPortal || !open || !dropdownRef.current || placement !== 'auto')
2940
- return;
2941
- const rect = dropdownRef.current.getBoundingClientRect();
2942
- const spaceBottom = window.innerHeight - rect.bottom;
2943
- const spaceRight = window.innerWidth - rect.right;
2944
- if (spaceBottom < 150)
2945
- setPosition('up');
2946
- else if (spaceRight < 150)
2947
- setPosition('start');
2948
- else
2949
- setPosition('down');
2950
- }, [asPortal, open, placement]);
2951
- // Compute + track coords: portal mode
2952
- // Re-runs whenever the menu opens and keeps coords fresh on scroll/resize
2953
- useEffect(() => {
2954
- if (!asPortal || !open)
3247
+ }, [open]);
3248
+ // Computes the initial position synchronously (before paint, to avoid a
3249
+ // flicker) and keeps it fresh while the menu is open and the viewport
3250
+ // scrolls or resizes (throttled to one update per frame, see above).
3251
+ useLayoutEffect(() => {
3252
+ if (!open)
2955
3253
  return () => { };
2956
- computePortalCoords();
2957
- const update = () => computePortalCoords();
2958
- window.addEventListener('scroll', update, { passive: true, capture: true });
2959
- window.addEventListener('resize', update, { passive: true });
3254
+ updatePosition();
3255
+ window.addEventListener('scroll', scheduleUpdatePosition, { passive: true, capture: true });
3256
+ window.addEventListener('resize', scheduleUpdatePosition, { passive: true });
2960
3257
  return () => {
2961
- window.removeEventListener('scroll', update, { capture: true });
2962
- window.removeEventListener('resize', update);
3258
+ window.removeEventListener('scroll', scheduleUpdatePosition, { capture: true });
3259
+ window.removeEventListener('resize', scheduleUpdatePosition);
3260
+ if (rafIdRef.current !== null) {
3261
+ window.cancelAnimationFrame(rafIdRef.current);
3262
+ rafIdRef.current = null;
3263
+ }
2963
3264
  };
2964
- }, [asPortal, open, computePortalCoords]);
3265
+ }, [open, updatePosition, scheduleUpdatePosition]);
2965
3266
  let ToggleElement;
2966
3267
  if (dropdownToggle) {
2967
3268
  ToggleElement = typeof dropdownToggle === 'function'
@@ -2971,57 +3272,30 @@ function DDropdown({ actions, dropdownToggle, className, classNameMenu, asPortal
2971
3272
  else {
2972
3273
  ToggleElement = (jsx(DButtonIcon, { variant: "link", stopPropagationEnabled: false, "aria-label": "Toggle Dropdown", "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle, icon: "MoreVertical" }));
2973
3274
  }
2974
- const portalMenuStyle = asPortal && menuCoords
3275
+ // Until the first measurement is available, keep the menu off-screen but
3276
+ // measurable (visibility: hidden preserves layout, unlike display: none).
3277
+ const menuStyle = coords
2975
3278
  ? {
2976
3279
  position: 'fixed',
2977
- top: menuCoords.top,
2978
- bottom: menuCoords.bottom,
2979
- left: menuCoords.left,
2980
- right: menuCoords.right,
2981
- minWidth: menuCoords.minWidth,
2982
- maxHeight: menuCoords.maxHeight,
3280
+ top: coords.top,
3281
+ left: coords.left,
3282
+ minWidth: coords.minWidth,
3283
+ maxHeight: coords.maxHeight,
2983
3284
  overflowY: 'auto',
2984
3285
  zIndex: 1050,
2985
3286
  transform: 'none',
3287
+ visibility: 'visible',
2986
3288
  }
2987
- : undefined;
2988
- let inlineTop = '100%';
2989
- if (resolvedInlinePosition === 'up') {
2990
- inlineTop = 'auto';
2991
- }
2992
- else if (resolvedInlinePosition === 'start' || resolvedInlinePosition === 'end') {
2993
- inlineTop = 0;
2994
- }
2995
- let inlineLeft = 0;
2996
- if (resolvedInlinePosition === 'start') {
2997
- inlineLeft = 'auto';
2998
- }
2999
- else if (resolvedInlinePosition === 'end') {
3000
- inlineLeft = '100%';
3001
- }
3002
- let inlineRight = 'auto';
3003
- if (resolvedInlinePosition === 'start') {
3004
- inlineRight = '100%';
3005
- }
3006
- let inlineTransform = 'translateY(4px)';
3007
- if (resolvedInlinePosition === 'up') {
3008
- inlineTransform = 'translateY(-4px)';
3009
- }
3010
- else if (resolvedInlinePosition === 'start') {
3011
- inlineTransform = 'translateX(-4px)';
3012
- }
3013
- else if (resolvedInlinePosition === 'end') {
3014
- inlineTransform = 'translateX(4px)';
3015
- }
3016
- const inlineMenuStyle = {
3017
- position: 'absolute',
3018
- top: inlineTop,
3019
- bottom: resolvedInlinePosition === 'up' ? '100%' : 'auto',
3020
- left: inlineLeft,
3021
- right: inlineRight,
3022
- transform: inlineTransform,
3023
- };
3024
- const menuItems = (jsx("ul", { ref: menuRef, role: "menu", className: classNames('dropdown-menu p-2', { show: open }, classNameMenu), style: portalMenuStyle !== null && portalMenuStyle !== void 0 ? portalMenuStyle : inlineMenuStyle, children: actions.map((action, index) => {
3289
+ : {
3290
+ position: 'fixed',
3291
+ top: 0,
3292
+ left: 0,
3293
+ visibility: 'hidden',
3294
+ pointerEvents: 'none',
3295
+ zIndex: 1050,
3296
+ transform: 'none',
3297
+ };
3298
+ const menuItems = (jsx("ul", { ref: menuRef, role: "menu", className: classNames('dropdown-menu p-2', { show: open }, classNameMenu), style: menuStyle, children: actions.map((action, index) => {
3025
3299
  if (action.isDivider) {
3026
3300
  return jsx("hr", { className: "dropdown-divider" }, index);
3027
3301
  }
@@ -3040,10 +3314,7 @@ function DDropdown({ actions, dropdownToggle, className, classNameMenu, asPortal
3040
3314
  }
3041
3315
  }, disabled: action.disabled, children: [action.icon && jsx(DIcon, { icon: action.icon, className: "me-2", size: "1rem" }), action.label] })) }, index));
3042
3316
  }) }));
3043
- if (asPortal) {
3044
- return (jsxs("div", { ref: toggleRef, className: classNames('dropdown', className), children: [ToggleElement, open && menuCoords && createPortal(menuItems, document.body)] }));
3045
- }
3046
- return (jsxs("div", { className: classNames(`dropdown drop-${resolvedInlinePosition}`, className), ref: dropdownRef, children: [ToggleElement, menuItems] }));
3317
+ return (jsxs("div", { ref: toggleRef, className: classNames(`dropdown drop-${resolvedSide} d-inline-block`, className), children: [ToggleElement, open && (asPortal ? createPortal(menuItems, document.body) : menuItems)] }));
3047
3318
  }
3048
3319
 
3049
3320
  function useScreenshot() {
@@ -3073,13 +3344,18 @@ function useScreenshot() {
3073
3344
 
3074
3345
  function useScreenshotDownload() {
3075
3346
  const { clipRef, takeBlob } = useScreenshot();
3076
- const download = useCallback(async () => {
3077
- const blob = await takeBlob();
3347
+ const download = useCallback(async (fileName = 'voucher') => {
3348
+ const blob = await takeBlob('image/jpeg');
3078
3349
  const url = window.URL.createObjectURL(blob);
3079
3350
  const link = window.document.createElement('a');
3080
3351
  link.style.display = 'none';
3081
3352
  link.href = url;
3082
- link.download = 'voucher.jpg';
3353
+ // Fall back to the default base name when the caller passes a blank/whitespace value,
3354
+ // so the file is never named just ".jpg".
3355
+ const safeFileName = fileName.trim() || 'voucher';
3356
+ // The extension is always appended by the hook to guarantee a valid image file,
3357
+ // regardless of what fileName the caller passes (e.g. "receipt.png" -> "receipt.png.jpg").
3358
+ link.download = `${safeFileName}.jpg`;
3083
3359
  document.body.appendChild(link);
3084
3360
  link.click();
3085
3361
  document.body.removeChild(link);
@@ -3093,11 +3369,15 @@ function useScreenshotDownload() {
3093
3369
 
3094
3370
  function useScreenshotWebShare() {
3095
3371
  const { takeBlob, clipRef } = useScreenshot();
3096
- const share = useCallback(async () => {
3097
- const blob = await takeBlob();
3098
- const image = new File([blob], 'voucher.jpeg', { type: 'image/jpeg' });
3099
- if (!navigator.canShare
3100
- && (navigator.canShare && !navigator.canShare({ files: [image] }))) {
3372
+ const share = useCallback(async (fileName = 'voucher') => {
3373
+ const blob = await takeBlob('image/jpeg');
3374
+ // Fall back to the default base name when the caller passes a blank/whitespace value,
3375
+ // so the file is never named just ".jpeg".
3376
+ const safeFileName = fileName.trim() || 'voucher';
3377
+ // The extension is always appended by the hook to guarantee a valid image file,
3378
+ // regardless of what fileName the caller passes (e.g. "receipt.png" -> "receipt.png.jpeg").
3379
+ const image = new File([blob], `${safeFileName}.jpeg`, { type: 'image/jpeg' });
3380
+ if (!navigator.canShare || !navigator.canShare({ files: [image] })) {
3101
3381
  window.print();
3102
3382
  return;
3103
3383
  }
@@ -3109,11 +3389,11 @@ function useScreenshotWebShare() {
3109
3389
  };
3110
3390
  }
3111
3391
 
3112
- function DVoucher({ amount, amountDetails, icon, title, onError, message, downloadText = 'Download', shareText = 'Share', className, children, }) {
3392
+ function DVoucher({ amount, amountDetails, icon, title, onError, message, downloadText = 'Download', shareText = 'Share', fileName = 'voucher', hideActions = false, className, children, }) {
3113
3393
  const { shareRef, share } = useScreenshotWebShare();
3114
3394
  const { downloadRef, download } = useScreenshotDownload();
3115
3395
  const handleShare = () => {
3116
- share()
3396
+ share(fileName)
3117
3397
  .catch(async (err) => {
3118
3398
  if (onError) {
3119
3399
  await onError(err);
@@ -3124,7 +3404,7 @@ function DVoucher({ amount, amountDetails, icon, title, onError, message, downlo
3124
3404
  });
3125
3405
  };
3126
3406
  const handleDownload = () => {
3127
- download()
3407
+ download(fileName)
3128
3408
  .catch(async (err) => {
3129
3409
  if (onError) {
3130
3410
  await onError(err);
@@ -3152,80 +3432,36 @@ function DVoucher({ amount, amountDetails, icon, title, onError, message, downlo
3152
3432
  return (jsx("div", { className: classNames('d-voucher', className), ref: (el) => {
3153
3433
  shareRef.current = el;
3154
3434
  downloadRef.current = el;
3155
- }, children: jsxs("div", { children: [jsxs("div", { className: "d-voucher-header", children: [resolvedIconProps && (jsx(DIcon, Object.assign({}, resolvedIconProps))), jsxs("div", { className: "text-center", children: [jsx("h3", { className: "mb-2", children: title }), jsx("p", { className: "m-0", children: message })] })] }), amount && (jsxs("div", { className: "d-voucher-amount", children: [jsx("div", { className: classNames('text-center fw-bold fs-3', amountDetails ? 'mb-1' : 'm-0'), children: amount }), amountDetails] })), jsx("hr", { className: "my-4" }), children, jsx("hr", { className: "my-4" }), jsxs("div", { className: "d-voucher-footer", children: [jsx(DButton, { onClick: handleShare, iconStart: "Share2", text: shareText, variant: "outline", size: "sm" }), jsx(DButton, { onClick: handleDownload, iconStart: "Download", text: downloadText, variant: "outline", size: "sm" })] })] }) }));
3156
- }
3157
-
3158
- function useCountdown(seconds) {
3159
- const [secondsLeft, setSecondsLeft] = useState(seconds);
3160
- const [isActive, setIsActive] = useState(true);
3161
- const resetCountdown = useCallback((newSeconds = seconds) => {
3162
- setIsActive(false);
3163
- setSecondsLeft(newSeconds);
3164
- }, [seconds]);
3165
- const restartCountdown = useCallback(() => {
3166
- resetCountdown(seconds);
3167
- setIsActive(true);
3168
- }, [resetCountdown, seconds]);
3169
- useEffect(() => {
3170
- if (!isActive) {
3171
- return () => { };
3172
- }
3173
- const interval = setInterval(() => {
3174
- setSecondsLeft((prevSeconds) => {
3175
- const newSeconds = prevSeconds - 1;
3176
- if (newSeconds <= 0) {
3177
- clearInterval(interval);
3178
- setIsActive(false);
3179
- return 0;
3180
- }
3181
- return newSeconds;
3182
- });
3183
- }, 1000);
3184
- return () => clearInterval(interval);
3185
- }, [isActive]);
3186
- return { secondsLeft, restartCountdown };
3435
+ }, children: jsxs("div", { children: [jsxs("div", { className: "d-voucher-header", children: [resolvedIconProps && (jsx(DIcon, Object.assign({}, resolvedIconProps))), jsxs("div", { className: "d-voucher-title-group", children: [jsx("h3", { className: "d-voucher-title", children: title }), jsx("p", { className: "d-voucher-message", children: message })] })] }), amount && (jsxs("div", { className: "d-voucher-amount", children: [jsx("div", { className: classNames('d-voucher-amount-value', { 'd-voucher-amount-value-with-details': !!amountDetails }), children: amount }), amountDetails] })), jsx("hr", { className: "d-voucher-divider" }), children, !hideActions && (jsxs(Fragment, { children: [jsx("hr", { className: "my-4" }), jsxs("div", { className: "d-voucher-footer", children: [jsx(DButton, { onClick: handleShare, iconStart: "Share2", text: shareText, variant: "outline", size: "sm" }), jsx(DButton, { onClick: handleDownload, iconStart: "Download", text: downloadText, variant: "outline", size: "sm" })] })] }))] }) }));
3187
3436
  }
3188
3437
 
3189
3438
  const defaultMessage = (secs) => (secs > 0
3190
3439
  ? `Didn't get any code? Resend in: ${secs}s`
3191
3440
  : "Didn't get any code?");
3192
- function OtpCountdown({ seconds, resendText, message, }) {
3193
- const { secondsLeft, restartCountdown } = useCountdown(seconds);
3194
- return (jsxs("div", { className: "d-flex gap-2 align-items-center", children: [jsx("p", { className: "mb-0 flex-1", children: message ? message(secondsLeft) : defaultMessage(secondsLeft) }), jsx(DButton, { text: resendText, variant: "link", className: "text-nowrap", disabled: secondsLeft > 0, onClick: restartCountdown })] }));
3441
+ function OtpCountdown({ secondsLeft, restartCountdown, resendText, message, }) {
3442
+ return (jsxs("div", { className: "d-otp-countdown", children: [jsx("p", { className: "d-otp-countdown-text", children: message ? message(secondsLeft) : defaultMessage(secondsLeft) }), jsx(DButton, { text: resendText, variant: "link", className: "text-nowrap", disabled: secondsLeft > 0, onClick: restartCountdown })] }));
3195
3443
  }
3196
3444
 
3197
3445
  const TEXT_PROPS = {
3198
3446
  resend: 'Resend',
3199
- resendText: 'Resend code',
3447
+ resendText: 'Resend',
3200
3448
  submit: 'Authorize and continue',
3201
3449
  title: 'We will send you a 6-digit code to your associated phone number so you can continue with your request.',
3450
+ invalidCode: 'Invalid code, please try again.',
3202
3451
  contact: (jsxs(Fragment, { children: [jsx("span", { children: "Problems with your digital token? Contact us" }), ' ', jsx("a", { href: "https://www.modyo.com", className: "link-primary text-nowrap", target: "_blank", rel: "noreferrer", children: "Contact us" })] })),
3203
3452
  };
3204
3453
  function DOtp({ className, action, isLoading, otpSize = 6, texts = TEXT_PROPS, seconds = 15, }) {
3205
- const [otp, setOtp] = useState('');
3206
- const [invalid, setInvalid] = useState(false);
3207
- const handler = useCallback(async () => {
3208
- if (otp.length < otpSize) {
3209
- setInvalid(true);
3210
- return;
3211
- }
3212
- setInvalid(false);
3213
- await action();
3214
- }, [
3215
- otp.length,
3216
- action,
3217
- otpSize,
3218
- ]);
3219
- return (jsxs("div", { className: className, children: [jsx("p", { children: texts.title }), jsxs("div", { className: "d-flex flex-column gap-6 pb-4 px-3", children: [jsxs("div", { className: "d-flex flex-column gap-6", children: [jsx(DInputPin, { className: "modal-otp-pin", characters: otpSize, onChange: (e) => setOtp(e), invalid: invalid && otp.length < otpSize, placeholder: "0" }), jsx(OtpCountdown, { seconds: seconds, resendText: texts.resend })] }), jsx("hr", { className: "m-0" }), jsxs("div", { className: "d-flex flex-column flex-lg-row gap-4 align-items-center", children: [jsx(DButton, { text: texts.submit, onClick: () => {
3220
- handler().catch((err) => {
3454
+ const { setOtp, invalid, submit, isLoading: isSubmitting, secondsLeft, restartCountdown, } = useOtp({ action, otpSize, seconds });
3455
+ return (jsxs("div", { className: className, children: [jsx("p", { children: texts.title }), jsxs("div", { className: "d-otp d-otp-content", children: [jsxs("div", { className: "d-otp-fields", children: [jsx(DInputPin, { className: "modal-otp-pin", characters: otpSize, onChange: (e) => setOtp(e), invalid: invalid, hint: invalid ? texts.invalidCode : undefined, placeholder: "0" }), jsx(OtpCountdown, { secondsLeft: secondsLeft, restartCountdown: restartCountdown, resendText: texts.resendText })] }), jsx("hr", { className: "d-otp-divider" }), jsxs("div", { className: "d-otp-footer", children: [jsx(DButton, { text: texts.submit, onClick: () => {
3456
+ submit().catch((err) => {
3221
3457
  // eslint-disable-next-line no-console
3222
3458
  console.error('Error in DOtp action:', err);
3223
3459
  });
3224
- }, loading: isLoading }), jsx("p", { className: "small ms-lg-auto mb-0", children: texts.contact })] })] })] }));
3460
+ }, loading: isLoading || isSubmitting }), jsx("p", { className: "d-otp-contact", children: texts.contact })] })] })] }));
3225
3461
  }
3226
3462
 
3227
3463
  function DefaultErrorBoundary({ resetErrorBoundary }) {
3228
- return (jsx(DAlert, { color: "danger", showClose: false, children: jsxs("div", { className: "d-flex align-items-center gap-2", children: [jsx("span", { children: "An unexpected error occurred." }), jsx(DButton, { color: "secondary", variant: "outline", size: "sm", onClick: resetErrorBoundary, children: "Retry" })] }) }));
3464
+ return (jsx(DAlert, { color: "danger", showClose: false, children: jsxs("div", { className: "d-error-boundary-content", children: [jsx("span", { children: "An unexpected error occurred." }), jsx(DButton, { color: "secondary", variant: "outline", size: "sm", onClick: resetErrorBoundary, children: "Retry" })] }) }));
3229
3465
  }
3230
3466
 
3231
3467
  function DErrorBoundary({ name, fallback, resetKeys, onReset, onError, children, }) {
@@ -3243,15 +3479,15 @@ function DErrorBoundary({ name, fallback, resetKeys, onReset, onError, children,
3243
3479
  }
3244
3480
 
3245
3481
  function ErrorState({ message, onRetry, retryMessage = 'Retry', color = 'danger', }) {
3246
- return (jsxs(DAlert, { color: color, className: "d-flex align-items-center gap-3", children: [jsx("div", { className: "flex-grow-1", children: jsx("p", { className: "mb-0", children: message !== null && message !== void 0 ? message : 'An unexpected error occurred.' }) }), onRetry && (jsx(DButton, { onClick: onRetry, text: retryMessage, variant: "outline", iconStart: "RefreshCw" }))] }));
3482
+ return (jsxs(DAlert, { color: color, className: "d-error-state", children: [jsx("div", { className: "d-error-state-content", children: jsx("p", { className: "d-error-state-message", children: message !== null && message !== void 0 ? message : 'An unexpected error occurred.' }) }), onRetry && (jsx(DButton, { onClick: onRetry, text: retryMessage, variant: "outline", iconStart: "RefreshCw" }))] }));
3247
3483
  }
3248
3484
 
3249
3485
  function EmptyState({ message, icon = 'FileText', actionText, onAction, }) {
3250
- return (jsxs("div", { className: "d-flex flex-column align-items-center justify-content-center p-5 text-center", children: [jsx(DIcon, { icon: icon, size: "3rem", className: "text-secondary mb-3" }), jsx("p", { className: "text-secondary mb-3", children: message !== null && message !== void 0 ? message : 'No data available.' }), actionText && onAction && (jsx(DButton, { onClick: onAction, text: actionText, variant: "outline" }))] }));
3486
+ return (jsxs("div", { className: "d-empty-state", children: [jsx(DIcon, { icon: icon, size: "3rem", className: "d-empty-state-icon" }), jsx("p", { className: "d-empty-state-message", children: message !== null && message !== void 0 ? message : 'No data available.' }), actionText && onAction && (jsx(DButton, { onClick: onAction, text: actionText, variant: "outline" }))] }));
3251
3487
  }
3252
3488
 
3253
3489
  function LoadingState({ ariaLabel = 'Loading...', className }) {
3254
- return (jsx("div", { className: `d-flex align-items-center justify-content-center p-4 ${className || ''}`.trim(), "aria-busy": "true", "aria-live": "polite", children: jsx("span", { className: "spinner-border", role: "status", "aria-label": ariaLabel }) }));
3490
+ return (jsx("div", { className: `d-loading-state ${className || ''}`.trim(), "aria-busy": "true", "aria-live": "polite", children: jsx("span", { className: "spinner-border", role: "status", "aria-label": ariaLabel }) }));
3255
3491
  }
3256
3492
 
3257
3493
  function render(renderable) {
@@ -3310,7 +3546,7 @@ function DConfirmModalUI({ entry }) {
3310
3546
  }
3311
3547
  });
3312
3548
  }, [onConfirmAction]);
3313
- return (jsxs(DModal$1, { name: entry.id, size: "lg", className: `confirm-modal ${critical ? 'critical-modal' : ''}`, children: [jsx(DModal$1.Header, { onClose: handleClose, showCloseButton: true, children: jsx("h5", { className: "fw-bold", children: title }) }), jsxs(DModal$1.Body, { className: "py-3 px-5", children: [jsx("p", { className: "mb-4", children: message }), critical && (jsx(ForwardedDInput, { type: "text", label: critical.codeLabel || 'Confirmation code', placeholder: critical.inputPlaceholder, value: confirmationCode, onChange: (value) => setConfirmationCode(value), className: "mb-4", autoFocus: true }))] }), jsxs(DModal$1.Footer, { children: [jsx(DButton, { text: cancelLabel, variant: "outline", color: "secondary", onClick: handleClose, disabled: internalLoading }), jsx(DButton, { text: confirmLabel, color: confirmColor, onClick: handleConfirmClick, disabled: isConfirmDisabled, loading: internalLoading })] })] }));
3549
+ return (jsxs(DModal$1, { name: entry.id, size: "lg", className: `confirm-modal ${critical ? 'critical-modal' : ''}`, children: [jsx(DModal$1.Header, { onClose: handleClose, showCloseButton: true, children: jsx("h5", { className: "fw-bold", children: title }) }), jsxs(DModal$1.Body, { className: "d-confirm-modal-body", children: [jsx("p", { className: "d-confirm-modal-message", children: message }), critical && (jsx(ForwardedDInput, { type: "text", label: critical.codeLabel || 'Confirmation code', placeholder: critical.inputPlaceholder, value: confirmationCode, onChange: (value) => setConfirmationCode(value), className: "d-confirm-modal-field", autoFocus: true }))] }), jsxs(DModal$1.Footer, { children: [jsx(DButton, { text: cancelLabel, variant: "outline", color: "secondary", onClick: handleClose, disabled: internalLoading }), jsx(DButton, { text: confirmLabel, color: confirmColor, onClick: handleConfirmClick, disabled: isConfirmDisabled, loading: internalLoading })] })] }));
3314
3550
  }
3315
3551
 
3316
3552
  /**
@@ -3363,5 +3599,5 @@ function DConfirmModalContainer({ nodeId }) {
3363
3599
  return createPortal(jsx(AnimatePresence, { children: entries.map((entry) => (jsxs(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0, transition: { delay: 0.3 } }, transition: { duration: 0.15, ease: 'linear' }, children: [jsx("div", { className: "backdrop backdrop-confirm-modal", onClick: entry.onCloseAction, role: "presentation" }), jsx(DConfirmModalUI, { entry: entry })] }, entry.id))) }), portalNode);
3364
3600
  }
3365
3601
 
3366
- export { DAlert, DAvatar, DBadge, DBox, DBoxFile, DButton, DButtonIcon, DCard$1 as DCard, DCardBody, DCardFooter, DCardHeader, DCarousel$1 as DCarousel, DCarouselSlide, DChip, DCollapse, DConfirmModalContainer, DContext, DContextProvider, DCreditCard, DCurrencyText, DDataStateWrapper, DDatePicker, DDropdown, DErrorBoundary, DIcon, DIconBase, ForwardedDInput as DInput, DInputCheck, ForwardedDInputCounter as DInputCounter, ForwardedDInputCurrency as DInputCurrency, ForwardedDInputMask as DInputMask, ForwardedDInputPassword as DInputPassword, ForwardedDInputPhone as DInputPhone, DInputPin, ForwardedDInputRange as DInputRange, ForwardedDInputSearch as DInputSearch, DInputSelect, DInputSwitch, DLayout$1 as DLayout, DLayoutPane, DListGroup$1 as DListGroup, DListGroupItem, DModal$1 as DModal, DModalBody, DModalFooter, DModalHeader, DOffcanvas$1 as DOffcanvas, DOffcanvasBody, DOffcanvasFooter, DOffcanvasHeader, DOtp, DPaginator, DPasswordStrengthMeter, DPopover, DProgress, DSelect$1 as DSelect, DStepper, DStepper$2 as DStepperDesktop, DStepper$1 as DStepperMobile, DTabContent, DTabs$1 as DTabs, DTimeline, DToast$1 as DToast, DToastContainer, DTooltip, DVoucher, EmptyState, ErrorState, LoadingState, changeQueryString, checkMediaQuery, configureI8n as configureI18n, formatCurrency, getCssVariable, getQueryString, subscribeToMediaQuery, useConfirmModal, useDContext, useDPortalContext, useDToast, useDisableBodyScrollEffect, useDisableInputWheel, useFormatCurrency, useInputCurrency, useItemSelection, useMediaBreakpointUpLg, useMediaBreakpointUpMd, useMediaBreakpointUpSm, useMediaBreakpointUpXl, useMediaBreakpointUpXs, useMediaBreakpointUpXxl, useMediaQuery, useProvidedRefOrCreate, useStackState, useTabContext, validatePhoneNumber };
3602
+ export { DAlert, DAvatar, DBadge, DBox, DBoxFile, DButton, DButtonIcon, DCard$1 as DCard, DCardBody, DCardFooter, DCardHeader, DCarousel$1 as DCarousel, DCarouselSlide, DChip, DCollapse, DConfirmModalContainer, DContext, DContextProvider, DCreditCard, DCurrencyText, DDataStateWrapper, DDatePicker, DDropdown, DErrorBoundary, DIcon, DIconBase, ForwardedDInput as DInput, DInputCheck, ForwardedDInputCounter as DInputCounter, ForwardedDInputCurrency as DInputCurrency, ForwardedDInputMask as DInputMask, ForwardedDInputPassword as DInputPassword, ForwardedDInputPhone as DInputPhone, DInputPin, ForwardedDInputRange as DInputRange, ForwardedDInputSearch as DInputSearch, DInputSelect, DInputSwitch, DLayout$1 as DLayout, DLayoutPane, DListGroup$1 as DListGroup, DListGroupItem, DModal$1 as DModal, DModalBody, DModalFooter, DModalHeader, DOffcanvas$1 as DOffcanvas, DOffcanvasBody, DOffcanvasFooter, DOffcanvasHeader, DOtp, DPaginator, DPasswordStrengthMeter, DPopover, DProgress, DSelect$1 as DSelect, DStepper, DStepper$2 as DStepperDesktop, DStepper$1 as DStepperMobile, DTabContent, DTabs$1 as DTabs, DTimeline, DToast$1 as DToast, DToastContainer, DTooltip, DVoucher, EmptyState, ErrorState, LoadingState, changeQueryString, checkMediaQuery, configureI8n as configureI18n, formatCurrency, getCssVariable, getQueryString, subscribeToMediaQuery, useConfirmModal, useCountdown, useDContext, useDPortalContext, useDToast, useDisableBodyScrollEffect, useDisableInputWheel, useFormatCurrency, useInputCurrency, useItemSelection, useMediaBreakpointUpLg, useMediaBreakpointUpMd, useMediaBreakpointUpSm, useMediaBreakpointUpXl, useMediaBreakpointUpXs, useMediaBreakpointUpXxl, useMediaQuery, useOtp, useProvidedRefOrCreate, useScreenshot, useScreenshotDownload, useScreenshotWebShare, useStackState, useTabContext, validatePhoneNumber };
3367
3603
  //# sourceMappingURL=index.esm.js.map