@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.js CHANGED
@@ -1126,7 +1126,7 @@ function DBoxFile(_a) {
1126
1126
  'd-box-file-invalid': isDragInvalid,
1127
1127
  }, className), style: style }, dataAttributes, { children: jsxRuntime.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: [jsxRuntime.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 && (jsxRuntime.jsx(DIcon, { icon: icon, familyClass: iconFamilyClass, familyPrefix: iconFamilyPrefix, materialStyle: iconMaterialStyle })), jsxRuntime.jsx("div", { className: "d-box-content", children: typeof children === 'function'
1128
1128
  ? children(openFileDialog)
1129
- : children || (jsxRuntime.jsx("p", { className: "text-center m-0", children: "Drag and drop some files here, or click to select files" })) })] })) })), !!files.length && (jsxRuntime.jsx("ul", { className: "d-box-files", children: files.map((file, index) => (jsxRuntime.jsx(ForwardedDInput, { value: file.name, iconStart: "Paperclip", iconEnd: "Trash", readOnly: true, onIconEndClick: () => handleRemoveFile(index) }, `${file.name} ${index}`))) }))] }));
1129
+ : children || (jsxRuntime.jsx("p", { className: "d-box-file-empty-text", children: "Drag and drop some files here, or click to select files" })) })] })) })), !!files.length && (jsxRuntime.jsx("ul", { className: "d-box-files", children: files.map((file, index) => (jsxRuntime.jsx(ForwardedDInput, { value: file.name, iconStart: "Paperclip", iconEnd: "Trash", readOnly: true, onIconEndClick: () => handleRemoveFile(index) }, `${file.name} ${index}`))) }))] }));
1130
1130
  }
1131
1131
 
1132
1132
  const DButton = React.forwardRef((props, ref) => {
@@ -1502,9 +1502,9 @@ function DDatePickerHeaderSelector({ date, changeYear, changeMonth, decreaseMont
1502
1502
  return (jsxRuntime.jsxs("div", { className: classNames('react-datepicker__header-selector react-datepicker__header-year-selector', className), style: style, children: [jsxRuntime.jsx(DButtonIcon, { icon: iconPrev || chevronLeft, size: iconSize, variant: "link", onClick: decreaseYear, disabled: prevYearButtonDisabled, "aria-label": prevYearAriaLabel, className: "header-button" }), jsxRuntime.jsx("p", { children: `${startYear} - ${endYear}` }), jsxRuntime.jsx(DButtonIcon, { icon: iconNext || chevronRight, size: iconSize, variant: "link", onClick: increaseYear, disabled: nextYearButtonDisabled, "aria-label": nextYearAriaLabel, className: "header-button" })] }));
1503
1503
  }
1504
1504
  if (pickerType === PickerType.Quarter || pickerType === PickerType.Month) {
1505
- return (jsxRuntime.jsxs("div", { className: classNames(`react-datepicker__header-selector react-datepicker__header-${pickerType}-selector`, className), style: style, children: [jsxRuntime.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' } }), jsxRuntime.jsx("div", { className: "d-flex justify-content-center flex-grow-1", children: showHeaderSelectors ? (jsxRuntime.jsx(DSelect, { options: years, value: defaultYear, defaultValue: defaultYear, onChange: (year) => changeYear(Number(year === null || year === void 0 ? void 0 : year.value)), searchable: false })) : (jsxRuntime.jsx("p", { children: defaultYear === null || defaultYear === void 0 ? void 0 : defaultYear.label })) }), jsxRuntime.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' } })] }));
1505
+ return (jsxRuntime.jsxs("div", { className: classNames(`react-datepicker__header-selector react-datepicker__header-${pickerType}-selector`, className), style: style, children: [jsxRuntime.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' } }), jsxRuntime.jsx("div", { className: "react-datepicker__header-year-selector", children: showHeaderSelectors ? (jsxRuntime.jsx(DSelect, { options: years, value: defaultYear, defaultValue: defaultYear, onChange: (year) => changeYear(Number(year === null || year === void 0 ? void 0 : year.value)), searchable: false })) : (jsxRuntime.jsx("p", { children: defaultYear === null || defaultYear === void 0 ? void 0 : defaultYear.label })) }), jsxRuntime.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' } })] }));
1506
1506
  }
1507
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "datepicker-top-header", children: [showHeaderSelectors && (jsxRuntime.jsx(DSelect, { 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" })), jsxRuntime.jsx("h4", { className: "mb-0 fw-normal", children: dateFns.format(monthDate, formatHeaderDate, { locale }) })] }), jsxRuntime.jsxs("div", { className: classNames('react-datepicker__header-selector react-datepicker__header-day-selector', className), style: style, children: [jsxRuntime.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 ? (jsxRuntime.jsx(DSelect, { 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" })) : (jsxRuntime.jsx("p", { children: `${defaultMonth.label} ${defaultYear === null || defaultYear === void 0 ? void 0 : defaultYear.label}` })), jsxRuntime.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' } })] })] }));
1507
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "datepicker-top-header", children: [showHeaderSelectors && (jsxRuntime.jsx(DSelect, { 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" })), jsxRuntime.jsx("h4", { className: "datepicker-top-header-title", children: dateFns.format(monthDate, formatHeaderDate, { locale }) })] }), jsxRuntime.jsxs("div", { className: classNames('react-datepicker__header-selector react-datepicker__header-day-selector', className), style: style, children: [jsxRuntime.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 ? (jsxRuntime.jsx(DSelect, { 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" })) : (jsxRuntime.jsx("p", { children: `${defaultMonth.label} ${defaultYear === null || defaultYear === void 0 ? void 0 : defaultYear.label}` })), jsxRuntime.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' } })] })] }));
1508
1508
  }
1509
1509
 
1510
1510
  function DDatePicker(_a) {
@@ -1639,11 +1639,24 @@ function formatValue(value, currencyOptions) {
1639
1639
  }
1640
1640
  return currency(value, Object.assign(Object.assign({}, currencyOptions), { symbol: '' })).format();
1641
1641
  }
1642
- function useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref) {
1642
+ function useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref, minValue, maxValue) {
1643
1643
  const inputRef = useProvidedRefOrCreate(ref);
1644
+ const clampValue = React.useCallback((newValue) => {
1645
+ if (newValue === undefined) {
1646
+ return newValue;
1647
+ }
1648
+ let clampedValue = newValue;
1649
+ if (minValue !== undefined) {
1650
+ clampedValue = Math.max(clampedValue, minValue);
1651
+ }
1652
+ if (maxValue !== undefined) {
1653
+ clampedValue = Math.min(clampedValue, maxValue);
1654
+ }
1655
+ return clampedValue;
1656
+ }, [minValue, maxValue]);
1644
1657
  const [innerType, setInnerType] = React.useState('text');
1645
- const [innerNumber, setInnerNumber] = React.useState(value);
1646
- const [innerString, setInnerString] = React.useState(formatValue(value, currencyOptions));
1658
+ const [innerNumber, setInnerNumber] = React.useState(clampValue(value));
1659
+ const [innerString, setInnerString] = React.useState(formatValue(clampValue(value), currencyOptions));
1647
1660
  const handleOnFocus = React.useCallback((event) => {
1648
1661
  event.stopPropagation();
1649
1662
  setInnerType('number');
@@ -1652,15 +1665,14 @@ function useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref
1652
1665
  const handleOnBlur = React.useCallback((event) => {
1653
1666
  event.stopPropagation();
1654
1667
  setInnerType('text');
1668
+ const clampedNumber = clampValue(innerNumber);
1669
+ if (clampedNumber !== innerNumber) {
1670
+ setInnerNumber(clampedNumber);
1671
+ setInnerString(formatValue(clampedNumber, currencyOptions));
1672
+ onChange === null || onChange === void 0 ? void 0 : onChange(clampedNumber);
1673
+ }
1655
1674
  onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
1656
- }, [onBlur]);
1657
- const generateStyleVariables = React.useMemo(() => ({
1658
- [`--${PREFIX_BS}input-currency-component-symbol-color`]: `var(--${PREFIX_BS}secondary)`,
1659
- [`--${PREFIX_BS}input-currency-symbol-color`]: `var(--${PREFIX_BS}input-currency-component-symbol-color)`,
1660
- }), []);
1661
- const generateSymbolStyleVariables = React.useMemo(() => ({
1662
- color: `var(--${PREFIX_BS}input-currency-symbol-color)`,
1663
- }), []);
1675
+ }, [onBlur, innerNumber, clampValue, currencyOptions, onChange]);
1664
1676
  const handleOnChange = React.useCallback((newValue) => {
1665
1677
  const newNumber = (newValue === undefined || newValue === '') ? undefined : Number(newValue);
1666
1678
  if (newNumber !== innerNumber) {
@@ -1669,7 +1681,12 @@ function useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref
1669
1681
  onChange === null || onChange === void 0 ? void 0 : onChange(newNumber);
1670
1682
  }
1671
1683
  }, [currencyOptions, onChange, innerNumber]);
1684
+ const isMountedRef = React.useRef(false);
1672
1685
  React.useEffect(() => {
1686
+ if (!isMountedRef.current) {
1687
+ isMountedRef.current = true;
1688
+ return;
1689
+ }
1673
1690
  if (value !== innerNumber) {
1674
1691
  setInnerNumber(value);
1675
1692
  setInnerString(formatValue(value, currencyOptions));
@@ -1683,8 +1700,6 @@ function useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref
1683
1700
  handleOnFocus,
1684
1701
  handleOnChange,
1685
1702
  handleOnBlur,
1686
- generateStyleVariables,
1687
- generateSymbolStyleVariables,
1688
1703
  };
1689
1704
  }
1690
1705
 
@@ -1789,6 +1804,149 @@ function useConfirmModal(config) {
1789
1804
  return { open };
1790
1805
  }
1791
1806
 
1807
+ /**
1808
+ * Generic seconds-based countdown hook.
1809
+ *
1810
+ * Starts counting down from `seconds` to `0` and exposes `restartCountdown`
1811
+ * to reset and restart the timer (e.g. for "resend code in Xs" patterns,
1812
+ * session-expiry banners, or any other cooldown/throttle UI).
1813
+ *
1814
+ * @example
1815
+ * const { secondsLeft, restartCountdown } = useCountdown(30);
1816
+ */
1817
+ function useCountdown(seconds) {
1818
+ const [secondsLeft, setSecondsLeft] = React.useState(seconds);
1819
+ const [isActive, setIsActive] = React.useState(true);
1820
+ const resetCountdown = React.useCallback((newSeconds = seconds) => {
1821
+ setIsActive(false);
1822
+ setSecondsLeft(newSeconds);
1823
+ }, [seconds]);
1824
+ const restartCountdown = React.useCallback(() => {
1825
+ resetCountdown(seconds);
1826
+ setIsActive(true);
1827
+ }, [resetCountdown, seconds]);
1828
+ React.useEffect(() => {
1829
+ if (!isActive) {
1830
+ return () => { };
1831
+ }
1832
+ const interval = setInterval(() => {
1833
+ setSecondsLeft((prevSeconds) => {
1834
+ const newSeconds = prevSeconds - 1;
1835
+ if (newSeconds <= 0) {
1836
+ clearInterval(interval);
1837
+ setIsActive(false);
1838
+ return 0;
1839
+ }
1840
+ return newSeconds;
1841
+ });
1842
+ }, 1000);
1843
+ return () => clearInterval(interval);
1844
+ }, [isActive]);
1845
+ return { secondsLeft, restartCountdown };
1846
+ }
1847
+
1848
+ /**
1849
+ * Headless hook exposing the same OTP logic used internally by `DOtp`
1850
+ * (code state, length + correctness validation, submit action and resend
1851
+ * countdown), without any bundled markup/styles.
1852
+ *
1853
+ * Use this when the default `DOtp` component doesn't match your design and
1854
+ * you need to build a fully custom UI while reusing the same behavior.
1855
+ *
1856
+ * Internally composes `useCountdown` for the resend timer and re-exposes
1857
+ * `secondsLeft`/`restartCountdown`, so there is no need to call
1858
+ * `useCountdown` separately.
1859
+ *
1860
+ * `invalid` becomes `true` both when the OTP is shorter than `otpSize` and
1861
+ * when `action` throws/rejects (e.g. the backend rejected the entered code
1862
+ * as incorrect). In the latter case, the thrown value is exposed as `error`
1863
+ * so custom UIs can render an appropriate message (e.g. "Invalid code" vs.
1864
+ * "Enter all digits"). `invalid`/`error` are cleared automatically as soon
1865
+ * as the user edits the code via `setOtp`.
1866
+ *
1867
+ * @example
1868
+ * function CustomOtp() {
1869
+ * const {
1870
+ * otp,
1871
+ * setOtp,
1872
+ * invalid,
1873
+ * error,
1874
+ * submit,
1875
+ * isLoading,
1876
+ * secondsLeft,
1877
+ * restartCountdown,
1878
+ * } = useOtp({ action: async () => verifyOtp(otp), otpSize: 6, seconds: 15 });
1879
+ *
1880
+ * return (
1881
+ * <>
1882
+ * <MyCustomPinInput value={otp} onChange={setOtp} invalid={invalid} />
1883
+ * {invalid && <span>{error ? 'Invalid code' : 'Enter all digits'}</span>}
1884
+ * <button disabled={isLoading} onClick={() => { submit().catch(() => {}); }}>
1885
+ * {isLoading ? 'Submitting...' : 'Submit'}
1886
+ * </button>
1887
+ * <button disabled={secondsLeft > 0} onClick={restartCountdown}>
1888
+ * Resend {secondsLeft > 0 ? `(${secondsLeft}s)` : ''}
1889
+ * </button>
1890
+ * </>
1891
+ * );
1892
+ * }
1893
+ */
1894
+ function useOtp({ action, otpSize = 6, seconds = 15, }) {
1895
+ const [otp, setOtpValue] = React.useState('');
1896
+ const [invalid, setInvalid] = React.useState(false);
1897
+ const [error, setError] = React.useState(null);
1898
+ const [isLoading, setIsLoading] = React.useState(false);
1899
+ const { secondsLeft, restartCountdown } = useCountdown(seconds);
1900
+ const setOtp = React.useCallback((value) => {
1901
+ setOtpValue((prevOtp) => {
1902
+ // Guard against no-op calls (e.g. DInputPin re-notifying the same
1903
+ // value when its onChange reference changes) so we don't wipe out
1904
+ // an `invalid`/`error` state that was just set by submit().
1905
+ if (prevOtp === value) {
1906
+ return prevOtp;
1907
+ }
1908
+ setInvalid(false);
1909
+ setError(null);
1910
+ return value;
1911
+ });
1912
+ }, []);
1913
+ const submit = React.useCallback(async () => {
1914
+ if (otp.length < otpSize) {
1915
+ setInvalid(true);
1916
+ setError(null);
1917
+ return;
1918
+ }
1919
+ setIsLoading(true);
1920
+ try {
1921
+ await action();
1922
+ setInvalid(false);
1923
+ setError(null);
1924
+ }
1925
+ catch (err) {
1926
+ setInvalid(true);
1927
+ setError(err);
1928
+ throw err;
1929
+ }
1930
+ finally {
1931
+ setIsLoading(false);
1932
+ }
1933
+ }, [
1934
+ otp.length,
1935
+ action,
1936
+ otpSize,
1937
+ ]);
1938
+ return {
1939
+ otp,
1940
+ setOtp,
1941
+ invalid,
1942
+ error,
1943
+ submit,
1944
+ isLoading,
1945
+ secondsLeft,
1946
+ restartCountdown,
1947
+ };
1948
+ }
1949
+
1792
1950
  function DInputCounter(_a, ref) {
1793
1951
  var { minValue, maxValue, value = minValue, invalid, iconStart: iconStartProp, iconEnd: iconEndProp, iconStartAriaLabel = 'decrease action', iconEndAriaLabel = 'increase action', style, onChange } = _a, props = tslib.__rest(_a, ["minValue", "maxValue", "value", "invalid", "iconStart", "iconEnd", "iconStartAriaLabel", "iconEndAriaLabel", "style", "onChange"]);
1794
1952
  const { handleOnWheel, } = useDisableInputWheel(ref);
@@ -1831,8 +1989,8 @@ function DInputCurrency(_a, ref) {
1831
1989
  var { value, minValue, maxValue, currencyCode, onFocus, onBlur, onChange } = _a, props = tslib.__rest(_a, ["value", "minValue", "maxValue", "currencyCode", "onFocus", "onBlur", "onChange"]);
1832
1990
  const { currency: currencyOptions } = useDContext();
1833
1991
  const { handleOnWheel, } = useDisableInputWheel(ref);
1834
- const { inputRef, innerValue, innerType, handleOnFocus, handleOnChange, handleOnBlur, generateStyleVariables, generateSymbolStyleVariables, } = useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref);
1835
- return (jsxRuntime.jsx(ForwardedDInput, Object.assign({ ref: inputRef, value: innerValue, onChange: handleOnChange, style: generateStyleVariables, inputMode: "decimal", type: innerType, onFocus: handleOnFocus, onBlur: handleOnBlur, onWheel: handleOnWheel, inputStart: (jsxRuntime.jsx("span", { slot: "input-start", style: generateSymbolStyleVariables, children: currencyCode || currencyOptions.symbol })) }, props)));
1992
+ const { inputRef, innerValue, innerType, handleOnFocus, handleOnChange, handleOnBlur, } = useInputCurrency(currencyOptions, value, onFocus, onChange, onBlur, ref, minValue, maxValue);
1993
+ return (jsxRuntime.jsx(ForwardedDInput, Object.assign({ ref: inputRef, value: innerValue, onChange: handleOnChange, inputMode: "decimal", type: innerType, onFocus: handleOnFocus, onBlur: handleOnBlur, onWheel: handleOnWheel, inputStart: (jsxRuntime.jsx("span", { slot: "input-start", className: "d-input-currency-symbol", children: currencyCode || currencyOptions.symbol })) }, props)));
1836
1994
  }
1837
1995
  const ForwardedDInputCurrency = React.forwardRef(DInputCurrency);
1838
1996
  ForwardedDInputCurrency.displayName = 'DInputCurrency';
@@ -1854,26 +2012,25 @@ ForwardedDInputPassword.displayName = 'DInputPassword';
1854
2012
 
1855
2013
  function PasswordCheckItem({ password, regex, text, }) {
1856
2014
  const isValid = regex.test(password);
1857
- return (jsxRuntime.jsxs("li", { className: "d-flex gap-2 align-items-start small text-gray-600", children: [jsxRuntime.jsx(DIcon, { className: classNames('flex-shrink-0', isValid ? 'text-success' : 'text-gray-300'), icon: isValid ? 'CircleCheck' : 'Circle', size: "16px" }), jsxRuntime.jsx("span", { className: classNames({ 'text-success': isValid }), children: text })] }));
2015
+ return (jsxRuntime.jsxs("li", { className: `d-password-check-item${isValid ? ' is-valid' : ''}`, children: [jsxRuntime.jsx(DIcon, { className: "d-password-check-item-icon", icon: isValid ? 'CircleCheck' : 'Circle', size: "16px" }), jsxRuntime.jsx("span", { children: text })] }));
1858
2016
  }
1859
2017
 
1860
- const getColorClass = (strength, total) => {
2018
+ const getStrengthModifier = (strength, total) => {
1861
2019
  const percentage = total > 0 ? strength / total : 0;
1862
2020
  if (percentage === 0)
1863
- return 'bg-gray-200';
2021
+ return 'is-empty';
1864
2022
  if (percentage <= 0.25)
1865
- return 'bg-danger';
2023
+ return 'is-weak';
1866
2024
  if (percentage <= 0.5)
1867
- return 'bg-warning';
2025
+ return 'is-fair';
1868
2026
  if (percentage <= 0.75)
1869
- return 'bg-info';
1870
- return 'bg-success';
2027
+ return 'is-good';
2028
+ return 'is-strong';
1871
2029
  };
1872
2030
  function PasswordStrengthBar({ strength, total }) {
1873
2031
  const percentage = total > 0 ? (strength / total) * 100 : 0;
1874
- return (jsxRuntime.jsx("div", { className: "w-100 rounded-3 overflow-hidden bg-gray-100 mb-2", style: { height: '8px' }, children: jsxRuntime.jsx("div", { className: `h-100 ${getColorClass(strength, total)}`, style: {
2032
+ return (jsxRuntime.jsx("div", { className: "d-password-strength-bar", children: jsxRuntime.jsx("div", { className: `d-password-strength-bar-fill ${getStrengthModifier(strength, total)}`, style: {
1875
2033
  width: `${percentage}%`,
1876
- transition: 'width 0.3s ease-in-out',
1877
2034
  } }) }));
1878
2035
  }
1879
2036
 
@@ -1909,7 +2066,7 @@ function PasswordChecksList({ password, validationMessages, enabledChecks, }) {
1909
2066
  const passwordChecks = allChecks.filter((check) => enabledChecks.includes(check.key));
1910
2067
  const passed = passwordChecks.filter((r) => r.regex.test(password)).length;
1911
2068
  const total = passwordChecks.length;
1912
- return (jsxRuntime.jsxs("div", { className: "mt-2", children: [jsxRuntime.jsx(PasswordStrengthBar, { strength: passed, total: total }), jsxRuntime.jsx("ul", { className: "list-unstyled m-0 d-flex flex-column gap-2", children: passwordChecks.map(({ key, regex, text }) => (jsxRuntime.jsx(PasswordCheckItem, { password: password, regex: regex, text: text }, key))) })] }));
2069
+ return (jsxRuntime.jsxs("div", { className: "d-password-strength-meter", children: [jsxRuntime.jsx(PasswordStrengthBar, { strength: passed, total: total }), jsxRuntime.jsx("ul", { className: "d-password-check-list", children: passwordChecks.map(({ key, regex, text }) => (jsxRuntime.jsx(PasswordCheckItem, { password: password, regex: regex, text: text }, key))) })] }));
1913
2070
  }
1914
2071
 
1915
2072
  const DEFAULT_VALIDATION_MESSAGES = {
@@ -2259,7 +2416,7 @@ var DListGroup = Object.assign(DListGroup$1, {
2259
2416
  function DModalHeader({ showCloseButton, onClose, children, className, style, iconFamilyClass, iconFamilyPrefix, icon: iconProp, materialStyle = false, }) {
2260
2417
  const { iconMap: { xLg, }, } = useDContext();
2261
2418
  const icon = React.useMemo(() => iconProp || xLg, [iconProp, xLg]);
2262
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: classNames('modal-header', className), style: style, children: [jsxRuntime.jsx("div", { children: children }), showCloseButton && (jsxRuntime.jsx("button", { type: "button", className: "d-close align-self-center", "aria-label": "Close", onClick: onClose, children: jsxRuntime.jsx(DIcon, { icon: icon, familyClass: iconFamilyClass, familyPrefix: iconFamilyPrefix, materialStyle: materialStyle }) }))] }), jsxRuntime.jsx("div", { className: "d-modal-separator" })] }));
2419
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: classNames('modal-header', className), style: style, children: [jsxRuntime.jsx("div", { children: children }), showCloseButton && (jsxRuntime.jsx("button", { type: "button", className: "d-close", "aria-label": "Close", onClick: onClose, children: jsxRuntime.jsx(DIcon, { icon: icon, familyClass: iconFamilyClass, familyPrefix: iconFamilyPrefix, materialStyle: materialStyle }) }))] }), jsxRuntime.jsx("div", { className: "d-modal-separator" })] }));
2263
2420
  }
2264
2421
 
2265
2422
  function DModalBody({ children, className, style, }) {
@@ -2303,7 +2460,7 @@ var DModal = Object.assign(DModal$1, {
2303
2460
  function DOffcanvasHeader({ showCloseButton, onClose, children, className, style, iconFamilyClass, iconFamilyPrefix, icon: iconProp, materialStyle = false, }) {
2304
2461
  const { iconMap: { xLg, }, } = useDContext();
2305
2462
  const icon = React.useMemo(() => iconProp || xLg, [iconProp, xLg]);
2306
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: classNames('offcanvas-header', className), style: style, children: [jsxRuntime.jsx("div", { children: children }), showCloseButton && (jsxRuntime.jsx("button", { type: "button", className: "d-close align-self-center", "aria-label": "Close", onClick: onClose, children: jsxRuntime.jsx(DIcon, { icon: icon, familyClass: iconFamilyClass, familyPrefix: iconFamilyPrefix, materialStyle: materialStyle }) }))] }), jsxRuntime.jsx("div", { className: "d-offcanvas-separator" })] }));
2463
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: classNames('offcanvas-header', className), style: style, children: [jsxRuntime.jsx("div", { children: children }), showCloseButton && (jsxRuntime.jsx("button", { type: "button", className: "d-close", "aria-label": "Close", onClick: onClose, children: jsxRuntime.jsx(DIcon, { icon: icon, familyClass: iconFamilyClass, familyPrefix: iconFamilyPrefix, materialStyle: materialStyle }) }))] }), jsxRuntime.jsx("div", { className: "d-offcanvas-separator" })] }));
2307
2464
  }
2308
2465
 
2309
2466
  function DOffcanvasBody({ children, className, style, }) {
@@ -2430,7 +2587,7 @@ function DPopover({ children, renderComponent, open, setOpen, adjustContentToRen
2430
2587
  }
2431
2588
  const reference = React.cloneElement(triggerElement, Object.assign({ ref: refs.setReference }, getReferenceProps(triggerElement.props)));
2432
2589
  return (jsxRuntime.jsxs("div", Object.assign({ className: classNames('d-popover', className), style: generateStyleVariables }, dataAttributes, { children: [reference, isOpen && (jsxRuntime.jsx(react.FloatingFocusManager, { context: context, modal: false, children: jsxRuntime.jsx("div", Object.assign({ className: classNames('d-popover-content', {
2433
- 'w-100': adjustContentToRender,
2590
+ 'd-popover-content-fill': adjustContentToRender,
2434
2591
  }), ref: refs.setFloating, style: floatingStyles, "aria-labelledby": headingId }, getFloatingProps(), { children: children })) }))] })));
2435
2592
  }
2436
2593
 
@@ -2494,13 +2651,13 @@ function DStepper$1({ options, currentStep, className, style, }) {
2494
2651
  }
2495
2652
 
2496
2653
  function DStepper({ options, currentStep, iconSuccess, iconSuccessFamilyClass, iconSuccessFamilyPrefix, iconSuccessMaterialStyle = false, vertical = false, breakpoint = 'lg', className, completed = false, style, dataAttributes, }) {
2497
- return (jsxRuntime.jsxs("div", Object.assign({ className: className, style: style }, dataAttributes, { children: [jsxRuntime.jsx("div", { className: `d-block d-${breakpoint}-none`, children: jsxRuntime.jsx(DStepper$1, { options: options, currentStep: currentStep }) }), jsxRuntime.jsx("div", { className: `d-none d-${breakpoint}-block`, children: jsxRuntime.jsx(DStepper$2, { options: options, currentStep: currentStep, vertical: vertical, iconSuccess: iconSuccess, iconSuccessFamilyClass: iconSuccessFamilyClass, iconSuccessFamilyPrefix: iconSuccessFamilyPrefix, iconSuccessMaterialStyle: iconSuccessMaterialStyle, completed: completed }) })] })));
2654
+ return (jsxRuntime.jsxs("div", Object.assign({ className: className, style: style }, dataAttributes, { children: [jsxRuntime.jsx("div", { className: classNames('d-stepper-mobile', `d-block d-${breakpoint}-none`), children: jsxRuntime.jsx(DStepper$1, { options: options, currentStep: currentStep }) }), jsxRuntime.jsx("div", { className: classNames('d-stepper-desktop', `d-none d-${breakpoint}-block`), children: jsxRuntime.jsx(DStepper$2, { options: options, currentStep: currentStep, vertical: vertical, iconSuccess: iconSuccess, iconSuccessFamilyClass: iconSuccessFamilyClass, iconSuccessFamilyPrefix: iconSuccessFamilyPrefix, iconSuccessMaterialStyle: iconSuccessMaterialStyle, completed: completed }) })] })));
2498
2655
  }
2499
2656
 
2500
2657
  const ARROW_WIDTH = 8;
2501
2658
  const ARROW_HEIGHT = 4;
2502
- const GAP = 2;
2503
- function DTooltip({ className, childrenClassName, style, offSet = ARROW_HEIGHT + GAP, padding, withFocus = false, withClick = false, withHover = true, open = false, placement = 'top', size, Component, children, }) {
2659
+ const GAP$1 = 2;
2660
+ function DTooltip({ className, childrenClassName, style, offSet = ARROW_HEIGHT + GAP$1, padding, withFocus = false, withClick = false, withHover = true, open = false, placement = 'top', size, Component, children, }) {
2504
2661
  const [isOpen, setIsOpen] = React.useState(open);
2505
2662
  const arrowRef = React.useRef(null);
2506
2663
  const { refs, context, floatingStyles, } = react.useFloating({
@@ -2569,8 +2726,16 @@ function DTabs$1({ children, defaultSelected, onChange, options, className, clas
2569
2726
  React.useEffect(() => {
2570
2727
  setSelected(defaultSelected);
2571
2728
  }, [defaultSelected]);
2572
- const generateClasses = React.useMemo(() => (Object.assign({ nav: true, 'flex-column align-items-center': vertical && variant !== 'tabs', [`nav-${variant}`]: true }, className && { [className]: true })), [vertical, variant, className]);
2729
+ const generateClasses = React.useMemo(() => (Object.assign({ nav: true, 'd-tabs-nav-vertical': vertical && variant !== 'tabs', [`nav-${variant}`]: true }, className && { [className]: true })), [vertical, variant, className]);
2573
2730
  const tabRefs = React.useRef([]);
2731
+ // Always holds the latest `options` without needing to be a dependency:
2732
+ // `options` is commonly passed as an inline array literal (e.g.
2733
+ // `options={[{ label: 'SMS', tab: 'sms' }, ...]}`), so it's a new array
2734
+ // reference on every parent render even when its content hasn't changed.
2735
+ // Reading it from this ref (updated synchronously on every render) lets
2736
+ // the focus effect below react only to `selected` changing.
2737
+ const optionsRef = React.useRef(options);
2738
+ optionsRef.current = options;
2574
2739
  React.useEffect(() => {
2575
2740
  tabRefs.current = options.map((_, i) => tabRefs.current[i] || React.createRef());
2576
2741
  }, [options]);
@@ -2585,20 +2750,28 @@ function DTabs$1({ children, defaultSelected, onChange, options, className, clas
2585
2750
  setSelected(firstEnabled.tab);
2586
2751
  }
2587
2752
  }, [options, selected]);
2588
- // Declarative focus management
2589
- const focusTab = (idx) => {
2753
+ // Declarative focus management. Wrapped in `useCallback` with an empty
2754
+ // dependency array since it only reads from the `tabRefs` ref, so its
2755
+ // identity stays stable across renders and it can safely be used as an
2756
+ // effect dependency below.
2757
+ const focusTab = React.useCallback((idx) => {
2590
2758
  var _a;
2591
2759
  if ((_a = tabRefs.current[idx]) === null || _a === void 0 ? void 0 : _a.current) {
2592
2760
  tabRefs.current[idx].current.focus();
2593
2761
  }
2594
- };
2595
- // Focus selected tab when selected changes
2762
+ }, []);
2763
+ // Focus selected tab when selected changes.
2764
+ // Reads `options` from `optionsRef` (see comment above) instead of
2765
+ // depending on `options` directly, so a parent re-render that merely
2766
+ // creates a new `options` reference with identical content doesn't call
2767
+ // `focusTab` again and steal focus away from unrelated elements on the
2768
+ // page (e.g. an OTP input).
2596
2769
  React.useEffect(() => {
2597
- const idx = options.findIndex((opt) => opt.tab === selected && !opt.disabled);
2770
+ const idx = optionsRef.current.findIndex((opt) => opt.tab === selected && !opt.disabled);
2598
2771
  if (idx !== -1) {
2599
2772
  focusTab(idx);
2600
2773
  }
2601
- }, [selected, options]);
2774
+ }, [selected, focusTab]);
2602
2775
  const handleKeyDown = React.useCallback((idx, e) => {
2603
2776
  const count = options.length;
2604
2777
  if (count === 0)
@@ -2627,7 +2800,7 @@ function DTabs$1({ children, defaultSelected, onChange, options, className, clas
2627
2800
  }
2628
2801
  }
2629
2802
  }
2630
- }, [options, vertical]);
2803
+ }, [options, vertical, focusTab]);
2631
2804
  let tablistProps = {};
2632
2805
  if (ariaLabelledBy) {
2633
2806
  tablistProps = { 'aria-labelledby': ariaLabelledBy };
@@ -2640,12 +2813,12 @@ function DTabs$1({ children, defaultSelected, onChange, options, className, clas
2640
2813
  isSelected,
2641
2814
  }), [isSelected]);
2642
2815
  return (jsxRuntime.jsx(TabContext.Provider, { value: value, children: jsxRuntime.jsxs("div", Object.assign({ className: classNames({
2643
- 'd-flex w-100': true,
2644
- 'flex-column': !vertical || variant === 'tabs',
2816
+ 'd-tabs': true,
2817
+ 'd-tabs-column': !vertical || variant === 'tabs',
2645
2818
  }), style: style }, dataAttributes, { children: [jsxRuntime.jsx("ul", Object.assign({ className: classNames(generateClasses), role: "tablist", "aria-orientation": vertical ? 'vertical' : undefined }, tablistProps, { children: options.map((option, idx) => {
2646
2819
  const isTabSelected = !!option.tab && option.tab === selected;
2647
2820
  return (jsxRuntime.jsx("li", { role: "presentation", className: "nav-item", children: jsxRuntime.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));
2648
- }) })), jsxRuntime.jsx("div", { className: "tab-content w-100", children: children })] })) }));
2821
+ }) })), jsxRuntime.jsx("div", { className: "d-tabs-content tab-content", children: children })] })) }));
2649
2822
  }
2650
2823
  var DTabs = Object.assign(DTabs$1, {
2651
2824
  Tab: DTabContent,
@@ -2707,11 +2880,11 @@ function useDToast() {
2707
2880
  if (!description) {
2708
2881
  return (jsxRuntime.jsx(DToast, { className: classNames({
2709
2882
  [`toast-${color}`]: !!color,
2710
- }, 'show'), children: jsxRuntime.jsxs(DToast.Body, { children: [icon && (jsxRuntime.jsx(DIcon, { className: "toast-icon", icon: icon })), jsxRuntime.jsx("p", { className: "toast-title", children: title }), jsxRuntime.jsx("button", { type: "button", className: "d-close align-self-center", "aria-label": "Close", onClick: () => reactHotToast.toast.dismiss(id), children: jsxRuntime.jsx(DIcon, { icon: closeIcon || xLg }) })] }) }));
2883
+ }, 'show'), children: jsxRuntime.jsxs(DToast.Body, { children: [icon && (jsxRuntime.jsx(DIcon, { className: "toast-icon", icon: icon })), jsxRuntime.jsx("p", { className: "toast-title", children: title }), jsxRuntime.jsx("button", { type: "button", className: "d-close", "aria-label": "Close", onClick: () => reactHotToast.toast.dismiss(id), children: jsxRuntime.jsx(DIcon, { icon: closeIcon || xLg }) })] }) }));
2711
2884
  }
2712
2885
  return (jsxRuntime.jsxs(DToast, { className: classNames({
2713
2886
  [`toast-${color}`]: !!color,
2714
- }, 'show'), children: [jsxRuntime.jsxs(DToast.Header, { children: [icon && (jsxRuntime.jsx(DIcon, { className: "toast-icon", icon: icon })), jsxRuntime.jsx("p", { className: "toast-title", children: title }), timestamp && (jsxRuntime.jsx("small", { className: "toast-timestamp", children: timestamp })), jsxRuntime.jsx("button", { type: "button", className: "d-close align-self-center", "aria-label": "Close", onClick: () => reactHotToast.toast.dismiss(id), children: jsxRuntime.jsx(DIcon, { icon: closeIcon || xLg }) })] }), jsxRuntime.jsx(DToast.Body, { children: jsxRuntime.jsx("span", { children: description }) })] }));
2887
+ }, 'show'), children: [jsxRuntime.jsxs(DToast.Header, { children: [icon && (jsxRuntime.jsx(DIcon, { className: "toast-icon", icon: icon })), jsxRuntime.jsx("p", { className: "toast-title", children: title }), timestamp && (jsxRuntime.jsx("small", { className: "toast-timestamp", children: timestamp })), jsxRuntime.jsx("button", { type: "button", className: "d-close", "aria-label": "Close", onClick: () => reactHotToast.toast.dismiss(id), children: jsxRuntime.jsx(DIcon, { icon: closeIcon || xLg }) })] }), jsxRuntime.jsx(DToast.Body, { children: jsxRuntime.jsx("span", { children: description }) })] }));
2715
2888
  }, toastProps);
2716
2889
  }, [xLg]);
2717
2890
  return {
@@ -2733,11 +2906,10 @@ async function configureI8n(resources, _a = {}) {
2733
2906
  .then((t) => t);
2734
2907
  }
2735
2908
 
2736
- function getQueryString(value, config = {
2737
- useSearch: true,
2738
- }) {
2739
- const urlParams = new URLSearchParams(config.useSearch ? window.location.search : '');
2740
- return urlParams.get(value) || config.default;
2909
+ function getQueryString(value, config = {}) {
2910
+ const { useSearch = true, default: defaultValue } = config;
2911
+ const urlParams = new URLSearchParams(useSearch ? window.location.search : '');
2912
+ return urlParams.get(value) || defaultValue;
2741
2913
  }
2742
2914
 
2743
2915
  function changeQueryString(values, { useSearch = true, pushState = false, } = {}) {
@@ -2860,89 +3032,228 @@ function DCreditCard({ brand = 'visa', name, number, holderText = 'Card Holder',
2860
3032
  return (jsxRuntime.jsxs("div", { className: classNames('d-credit-card', isVertical && 'is-vertical', className), children: [jsxRuntime.jsxs("div", { className: "d-credit-card-header", children: [jsxRuntime.jsx("img", { src: logoImage || BRAND_LOGOS[brand] || DEFAULT_IMAGE, alt: brand, className: "d-credit-card-logo", width: 100 }), isChipVisible && (jsxRuntime.jsx("div", { className: "d-credit-card-chip", children: jsxRuntime.jsx("img", { src: CHIP_IMAGE, alt: "chip", width: 30, className: "d-credit-card-chip-image" }) }))] }), jsxRuntime.jsxs("div", { className: "d-credit-card-details", children: [jsxRuntime.jsx("div", { className: "d-credit-card-number", children: number }), jsxRuntime.jsx("small", { className: "d-credit-card-holder-text", children: holderText }), jsxRuntime.jsx("span", { className: "d-credit-card-name", children: name })] })] }));
2861
3033
  }
2862
3034
 
3035
+ // Gap between the toggle and the menu.
3036
+ const GAP = 4;
3037
+ // Minimum distance the menu should keep from the viewport edges.
3038
+ const VIEWPORT_PADDING = 8;
3039
+ // Minimum usable height left for the menu before we consider a side unusable.
3040
+ const MIN_USABLE_SPACE = 120;
2863
3041
  const getItemClass = (action) => classNames({
2864
3042
  'dropdown-item d-flex align-items-center': true,
2865
3043
  [`dropdown-item-${action.color}`]: !!action.color,
2866
3044
  disabled: action.disabled,
2867
3045
  });
2868
- function DDropdown({ actions, dropdownToggle, className, classNameMenu, asPortal = false, placement = 'auto', }) {
3046
+ const clamp = (value, min, max) => {
3047
+ if (max < min)
3048
+ return min;
3049
+ return Math.min(Math.max(value, min), max);
3050
+ };
3051
+ /**
3052
+ * `position: fixed` is normally relative to the viewport, but any ancestor
3053
+ * with a `transform`, `perspective`, `filter`, `will-change` (of those) or a
3054
+ * non-trivial `contain` establishes a new containing block, making `fixed`
3055
+ * behave like `absolute` relative to that ancestor instead (e.g. Storybook's
3056
+ * docs canvas wrapper, animated cards, sticky containers, etc.). We detect
3057
+ * that ancestor so the computed coordinates can be adjusted accordingly,
3058
+ * keeping the menu correctly anchored to the toggle in every context.
3059
+ */
3060
+ const getFixedPositioningOffset = (fromEl) => {
3061
+ var _a;
3062
+ let node = (_a = fromEl === null || fromEl === void 0 ? void 0 : fromEl.parentElement) !== null && _a !== void 0 ? _a : null;
3063
+ while (node && node !== document.body) {
3064
+ const cs = window.getComputedStyle(node);
3065
+ // Treat empty/undefined values as "unset" (jsdom returns "" rather than
3066
+ // the CSS initial value like "none" for some of these properties).
3067
+ const isSet = (value) => !!value && value !== 'none';
3068
+ const establishesContainingBlock = isSet(cs.transform)
3069
+ || isSet(cs.perspective)
3070
+ || isSet(cs.filter)
3071
+ || isSet(cs.backdropFilter)
3072
+ || ['transform', 'perspective', 'filter'].some((prop) => { var _a; return (_a = cs.willChange) === null || _a === void 0 ? void 0 : _a.includes(prop); })
3073
+ || cs.contain.split(' ').some((value) => ['layout', 'paint', 'strict', 'content'].includes(value));
3074
+ if (establishesContainingBlock) {
3075
+ const rect = node.getBoundingClientRect();
3076
+ return { top: rect.top, left: rect.left };
3077
+ }
3078
+ node = node.parentElement;
3079
+ }
3080
+ return { top: 0, left: 0 };
3081
+ };
3082
+ /**
3083
+ * Resolves where the menu should render (flip) and where it should sit along
3084
+ * the cross axis (shift), so it always stays fully inside the viewport
3085
+ * regardless of where the toggle is placed or how the viewport is resized.
3086
+ * This mimics floating-ui's `flip` + `shift` middlewares without adding the
3087
+ * dependency, keeping the existing Bootstrap markup/classes.
3088
+ */
3089
+ const computeCoords = (toggleRect, menuSize, placement, alignment) => {
3090
+ const viewportWidth = window.innerWidth;
3091
+ const viewportHeight = window.innerHeight;
3092
+ const spaceBelow = viewportHeight - toggleRect.bottom;
3093
+ const spaceAbove = toggleRect.top;
3094
+ const spaceLeft = toggleRect.left;
3095
+ const spaceRight = viewportWidth - toggleRect.right;
3096
+ let side = placement === 'auto'
3097
+ ? 'down'
3098
+ : placement;
3099
+ if (placement === 'auto') {
3100
+ side = spaceBelow >= menuSize.height + GAP || spaceBelow >= spaceAbove ? 'down' : 'up';
3101
+ }
3102
+ // Flip within the current axis if the chosen side can't fit the menu but
3103
+ // the opposite side (same axis) can.
3104
+ if (side === 'down' && spaceBelow < menuSize.height + GAP && spaceAbove > spaceBelow) {
3105
+ side = 'up';
3106
+ }
3107
+ else if (side === 'up' && spaceAbove < menuSize.height + GAP && spaceBelow > spaceAbove) {
3108
+ side = 'down';
3109
+ }
3110
+ else if (side === 'start' && spaceLeft < menuSize.width + GAP && spaceRight > spaceLeft) {
3111
+ side = 'end';
3112
+ }
3113
+ else if (side === 'end' && spaceRight < menuSize.width + GAP && spaceLeft > spaceRight) {
3114
+ side = 'start';
3115
+ }
3116
+ // Cross-axis fallback: e.g. a horizontally-centered toggle on a narrow
3117
+ // viewport can't open sideways (neither `start` nor `end` fit), but there
3118
+ // is plenty of room above/below — switch the whole axis in that case
3119
+ // (and vice versa for `down`/`up` squeezed against a short viewport).
3120
+ const bestVerticalSpace = Math.max(spaceBelow, spaceAbove);
3121
+ const bestHorizontalSpace = Math.max(spaceLeft, spaceRight);
3122
+ if (side === 'down' || side === 'up') {
3123
+ if (bestVerticalSpace < MIN_USABLE_SPACE && bestHorizontalSpace > bestVerticalSpace) {
3124
+ side = spaceRight >= spaceLeft ? 'end' : 'start';
3125
+ }
3126
+ }
3127
+ else if (bestHorizontalSpace < menuSize.width + GAP
3128
+ && bestVerticalSpace > bestHorizontalSpace) {
3129
+ side = spaceBelow >= spaceAbove ? 'down' : 'up';
3130
+ }
3131
+ if (side === 'down' || side === 'up') {
3132
+ const top = side === 'down'
3133
+ ? toggleRect.bottom + GAP
3134
+ : toggleRect.top - menuSize.height - GAP;
3135
+ // Align horizontally per the requested `alignment` (start/end/center), then
3136
+ // fall back to the opposite edge if the preferred alignment would overflow
3137
+ // the viewport, and clamp as a final safety net so it never overflows.
3138
+ let left;
3139
+ if (alignment === 'end') {
3140
+ left = toggleRect.right - menuSize.width;
3141
+ }
3142
+ else if (alignment === 'center') {
3143
+ left = toggleRect.left + (toggleRect.width / 2) - (menuSize.width / 2);
3144
+ }
3145
+ else {
3146
+ left = toggleRect.left;
3147
+ }
3148
+ if (left + menuSize.width > viewportWidth - VIEWPORT_PADDING && alignment !== 'end') {
3149
+ const altLeft = toggleRect.right - menuSize.width;
3150
+ if (altLeft >= VIEWPORT_PADDING)
3151
+ left = altLeft;
3152
+ }
3153
+ else if (left < VIEWPORT_PADDING && alignment !== 'start') {
3154
+ const altLeft = toggleRect.left;
3155
+ if (altLeft + menuSize.width <= viewportWidth - VIEWPORT_PADDING)
3156
+ left = altLeft;
3157
+ }
3158
+ left = clamp(left, VIEWPORT_PADDING, Math.max(viewportWidth - menuSize.width - VIEWPORT_PADDING, VIEWPORT_PADDING));
3159
+ const availableHeight = side === 'down' ? spaceBelow : spaceAbove;
3160
+ // Cap strictly to the space actually available on the resolved side —
3161
+ // MIN_USABLE_SPACE is only meant to influence the flip/shift decision
3162
+ // above, not to force a taller box than what physically fits and
3163
+ // overflow the viewport.
3164
+ const maxUsableHeight = Math.max(availableHeight - GAP - VIEWPORT_PADDING, 0);
3165
+ return {
3166
+ top,
3167
+ left,
3168
+ minWidth: Math.max(toggleRect.width, 160),
3169
+ maxHeight: `${maxUsableHeight}px`,
3170
+ resolvedSide: side,
3171
+ };
3172
+ }
3173
+ // side is 'start' or 'end'
3174
+ let left = side === 'start'
3175
+ ? toggleRect.left - menuSize.width - GAP
3176
+ : toggleRect.right + GAP;
3177
+ // Safety-net clamp: in rare cases where the toggle is squeezed so tightly
3178
+ // that no side of any axis has enough room, keep the menu inside the
3179
+ // viewport rather than letting it render off-screen.
3180
+ left = clamp(left, VIEWPORT_PADDING, Math.max(viewportWidth - menuSize.width - VIEWPORT_PADDING, VIEWPORT_PADDING));
3181
+ // Shift vertically: align with the toggle's top edge by default, but switch
3182
+ // to the bottom edge (or clamp) if that would overflow the viewport.
3183
+ let { top } = toggleRect;
3184
+ if (top + menuSize.height > viewportHeight - VIEWPORT_PADDING) {
3185
+ top = toggleRect.bottom - menuSize.height;
3186
+ }
3187
+ top = clamp(top, VIEWPORT_PADDING, Math.max(viewportHeight - menuSize.height - VIEWPORT_PADDING, VIEWPORT_PADDING));
3188
+ return {
3189
+ top,
3190
+ left,
3191
+ minWidth: Math.max(toggleRect.width, 160),
3192
+ maxHeight: `${Math.max(viewportHeight - top - VIEWPORT_PADDING, 0)}px`,
3193
+ resolvedSide: side,
3194
+ };
3195
+ };
3196
+ function DDropdown({ actions, dropdownToggle, className, classNameMenu, asPortal = false, placement = 'auto', alignment = 'start', }) {
2869
3197
  const [open, setOpen] = React.useState(false);
2870
- const [position, setPosition] = React.useState('down');
2871
- const [menuCoords, setMenuCoords] = React.useState(null);
2872
- // Used in default (non-portal) mode: wraps the entire dropdown
2873
- const dropdownRef = React.useRef(null);
2874
- // Used in portal mode: ref on the toggle wrapper to read its viewport coords
3198
+ const [coords, setCoords] = React.useState(null);
3199
+ // Initialize from `placement` (falling back to 'down' for 'auto') so the
3200
+ // wrapper's `drop-*` class reflects the requested side even before the
3201
+ // first open/measurement, matching the previous `drop-${placement}` class
3202
+ // behavior (relevant for Bootstrap caret/orientation styling).
3203
+ const [resolvedSide, setResolvedSide] = React.useState(placement === 'auto' ? 'down' : placement);
3204
+ // Wraps the toggle in both modes — used to read its viewport-relative rect.
2875
3205
  const toggleRef = React.useRef(null);
2876
- // Ref on the rendered <ul> — used for outside-click detection in portal mode
3206
+ // Ref on the rendered <ul> — used to measure its size and for outside-click detection.
2877
3207
  const menuRef = React.useRef(null);
2878
3208
  const toggle = () => {
2879
- setMenuCoords(null);
3209
+ setCoords(null);
2880
3210
  setOpen((prev) => !prev);
2881
3211
  };
2882
- const resolvedInlinePosition = placement === 'auto' ? position : placement;
2883
- // Compute fixed coords from the toggle's bounding rect (portal mode only)
2884
- const computePortalCoords = React.useCallback(() => {
2885
- if (!toggleRef.current)
3212
+ // Measures the toggle + menu and (re)computes fixed, pixel-perfect coords
3213
+ // with collision detection, so the menu never overflows the viewport.
3214
+ const updatePosition = React.useCallback(() => {
3215
+ if (!toggleRef.current || !menuRef.current)
3216
+ return;
3217
+ const toggleRect = toggleRef.current.getBoundingClientRect();
3218
+ // If the toggle itself has scrolled completely out of the viewport
3219
+ // (regardless of `placement`), close the menu instead of leaving it
3220
+ // rendered detached from the toggle that opened it.
3221
+ const toggleIsMeasured = toggleRect.width > 0 || toggleRect.height > 0;
3222
+ const toggleIsOutOfView = toggleRect.bottom <= 0
3223
+ || toggleRect.top >= window.innerHeight
3224
+ || toggleRect.right <= 0
3225
+ || toggleRect.left >= window.innerWidth;
3226
+ if (toggleIsMeasured && toggleIsOutOfView) {
3227
+ setOpen(false);
2886
3228
  return;
2887
- const rect = toggleRef.current.getBoundingClientRect();
2888
- const spaceRight = window.innerWidth - rect.right;
2889
- const spaceBottom = window.innerHeight - rect.bottom;
2890
- const spaceTop = rect.top;
2891
- let resolvedPlacement;
2892
- if (placement === 'auto') {
2893
- if (spaceBottom < 150) {
2894
- resolvedPlacement = 'up';
2895
- }
2896
- else if (spaceRight < 150) {
2897
- resolvedPlacement = 'start';
2898
- }
2899
- else {
2900
- resolvedPlacement = 'down';
2901
- }
2902
- }
2903
- else {
2904
- resolvedPlacement = placement;
2905
- }
2906
- const openStart = resolvedPlacement === 'start';
2907
- const openEnd = resolvedPlacement === 'end';
2908
- const openUp = resolvedPlacement === 'up';
2909
- let availableHeight = spaceBottom;
2910
- if (openUp) {
2911
- availableHeight = spaceTop;
2912
- }
2913
- else if (openStart || openEnd) {
2914
- availableHeight = window.innerHeight - rect.top;
2915
- }
2916
- const verticalCoords = openUp
2917
- ? { bottom: window.innerHeight - rect.top + 4 }
2918
- : { top: openStart || openEnd ? rect.top : rect.bottom + 4 };
2919
- let horizontalCoords;
2920
- if (openStart) {
2921
- horizontalCoords = { right: window.innerWidth - rect.left + 4 };
2922
- }
2923
- else if (openEnd) {
2924
- horizontalCoords = { left: rect.right + 4 };
2925
- }
2926
- else {
2927
- horizontalCoords = { left: rect.left };
2928
3229
  }
2929
- setMenuCoords(Object.assign(Object.assign(Object.assign({}, verticalCoords), horizontalCoords), { minWidth: Math.max(rect.width, 160), maxHeight: `${Math.max(availableHeight - 12, 120)}px` }));
2930
- }, [placement]);
2931
- // Outside-click: default mode
2932
- React.useEffect(() => {
2933
- if (asPortal)
2934
- return () => { };
2935
- const handler = (event) => {
2936
- if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
2937
- setOpen(false);
2938
- }
2939
- };
2940
- document.addEventListener('mousedown', handler);
2941
- return () => document.removeEventListener('mousedown', handler);
2942
- }, [asPortal]);
2943
- // Outside-click: portal mode (must check both toggle and floating menu)
2944
- React.useEffect(() => {
2945
- if (!asPortal || !open)
3230
+ const menuRect = menuRef.current.getBoundingClientRect();
3231
+ const _a = computeCoords(toggleRect, { width: menuRect.width, height: menuRect.height }, placement, alignment), { resolvedSide: side } = _a, menuCoords = tslib.__rest(_a, ["resolvedSide"]);
3232
+ // In portal mode the menu is appended to document.body, whose containing
3233
+ // block for `fixed` is always the viewport (no adjustment needed). In
3234
+ // inline mode the menu stays next to the toggle, so we must look for a
3235
+ // containing-block-establishing ancestor starting from the toggle.
3236
+ const offset = asPortal
3237
+ ? { top: 0, left: 0 }
3238
+ : getFixedPositioningOffset(toggleRef.current);
3239
+ setResolvedSide(side);
3240
+ setCoords(Object.assign(Object.assign({}, menuCoords), { top: menuCoords.top - offset.top, left: menuCoords.left - offset.left }));
3241
+ }, [placement, alignment, asPortal]);
3242
+ // Throttles scroll/resize-triggered updates to at most once per frame,
3243
+ // avoiding repeated forced reflows on busy/high-frequency scroll events.
3244
+ const rafIdRef = React.useRef(null);
3245
+ const scheduleUpdatePosition = React.useCallback(() => {
3246
+ if (rafIdRef.current !== null)
3247
+ return;
3248
+ rafIdRef.current = window.requestAnimationFrame(() => {
3249
+ rafIdRef.current = null;
3250
+ updatePosition();
3251
+ });
3252
+ }, [updatePosition]);
3253
+ // Outside-click detection (checks both the toggle and the floating menu,
3254
+ // since in portal mode the menu isn't a DOM descendant of the toggle).
3255
+ React.useLayoutEffect(() => {
3256
+ if (!open)
2946
3257
  return () => { };
2947
3258
  const handler = (event) => {
2948
3259
  var _a, _b;
@@ -2953,35 +3264,25 @@ function DDropdown({ actions, dropdownToggle, className, classNameMenu, asPortal
2953
3264
  };
2954
3265
  document.addEventListener('mousedown', handler);
2955
3266
  return () => document.removeEventListener('mousedown', handler);
2956
- }, [asPortal, open]);
2957
- // Auto-position: default mode
2958
- React.useEffect(() => {
2959
- if (asPortal || !open || !dropdownRef.current || placement !== 'auto')
2960
- return;
2961
- const rect = dropdownRef.current.getBoundingClientRect();
2962
- const spaceBottom = window.innerHeight - rect.bottom;
2963
- const spaceRight = window.innerWidth - rect.right;
2964
- if (spaceBottom < 150)
2965
- setPosition('up');
2966
- else if (spaceRight < 150)
2967
- setPosition('start');
2968
- else
2969
- setPosition('down');
2970
- }, [asPortal, open, placement]);
2971
- // Compute + track coords: portal mode
2972
- // Re-runs whenever the menu opens and keeps coords fresh on scroll/resize
2973
- React.useEffect(() => {
2974
- if (!asPortal || !open)
3267
+ }, [open]);
3268
+ // Computes the initial position synchronously (before paint, to avoid a
3269
+ // flicker) and keeps it fresh while the menu is open and the viewport
3270
+ // scrolls or resizes (throttled to one update per frame, see above).
3271
+ React.useLayoutEffect(() => {
3272
+ if (!open)
2975
3273
  return () => { };
2976
- computePortalCoords();
2977
- const update = () => computePortalCoords();
2978
- window.addEventListener('scroll', update, { passive: true, capture: true });
2979
- window.addEventListener('resize', update, { passive: true });
3274
+ updatePosition();
3275
+ window.addEventListener('scroll', scheduleUpdatePosition, { passive: true, capture: true });
3276
+ window.addEventListener('resize', scheduleUpdatePosition, { passive: true });
2980
3277
  return () => {
2981
- window.removeEventListener('scroll', update, { capture: true });
2982
- window.removeEventListener('resize', update);
3278
+ window.removeEventListener('scroll', scheduleUpdatePosition, { capture: true });
3279
+ window.removeEventListener('resize', scheduleUpdatePosition);
3280
+ if (rafIdRef.current !== null) {
3281
+ window.cancelAnimationFrame(rafIdRef.current);
3282
+ rafIdRef.current = null;
3283
+ }
2983
3284
  };
2984
- }, [asPortal, open, computePortalCoords]);
3285
+ }, [open, updatePosition, scheduleUpdatePosition]);
2985
3286
  let ToggleElement;
2986
3287
  if (dropdownToggle) {
2987
3288
  ToggleElement = typeof dropdownToggle === 'function'
@@ -2991,57 +3292,30 @@ function DDropdown({ actions, dropdownToggle, className, classNameMenu, asPortal
2991
3292
  else {
2992
3293
  ToggleElement = (jsxRuntime.jsx(DButtonIcon, { variant: "link", stopPropagationEnabled: false, "aria-label": "Toggle Dropdown", "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle, icon: "MoreVertical" }));
2993
3294
  }
2994
- const portalMenuStyle = asPortal && menuCoords
3295
+ // Until the first measurement is available, keep the menu off-screen but
3296
+ // measurable (visibility: hidden preserves layout, unlike display: none).
3297
+ const menuStyle = coords
2995
3298
  ? {
2996
3299
  position: 'fixed',
2997
- top: menuCoords.top,
2998
- bottom: menuCoords.bottom,
2999
- left: menuCoords.left,
3000
- right: menuCoords.right,
3001
- minWidth: menuCoords.minWidth,
3002
- maxHeight: menuCoords.maxHeight,
3300
+ top: coords.top,
3301
+ left: coords.left,
3302
+ minWidth: coords.minWidth,
3303
+ maxHeight: coords.maxHeight,
3003
3304
  overflowY: 'auto',
3004
3305
  zIndex: 1050,
3005
3306
  transform: 'none',
3307
+ visibility: 'visible',
3006
3308
  }
3007
- : undefined;
3008
- let inlineTop = '100%';
3009
- if (resolvedInlinePosition === 'up') {
3010
- inlineTop = 'auto';
3011
- }
3012
- else if (resolvedInlinePosition === 'start' || resolvedInlinePosition === 'end') {
3013
- inlineTop = 0;
3014
- }
3015
- let inlineLeft = 0;
3016
- if (resolvedInlinePosition === 'start') {
3017
- inlineLeft = 'auto';
3018
- }
3019
- else if (resolvedInlinePosition === 'end') {
3020
- inlineLeft = '100%';
3021
- }
3022
- let inlineRight = 'auto';
3023
- if (resolvedInlinePosition === 'start') {
3024
- inlineRight = '100%';
3025
- }
3026
- let inlineTransform = 'translateY(4px)';
3027
- if (resolvedInlinePosition === 'up') {
3028
- inlineTransform = 'translateY(-4px)';
3029
- }
3030
- else if (resolvedInlinePosition === 'start') {
3031
- inlineTransform = 'translateX(-4px)';
3032
- }
3033
- else if (resolvedInlinePosition === 'end') {
3034
- inlineTransform = 'translateX(4px)';
3035
- }
3036
- const inlineMenuStyle = {
3037
- position: 'absolute',
3038
- top: inlineTop,
3039
- bottom: resolvedInlinePosition === 'up' ? '100%' : 'auto',
3040
- left: inlineLeft,
3041
- right: inlineRight,
3042
- transform: inlineTransform,
3043
- };
3044
- const menuItems = (jsxRuntime.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) => {
3309
+ : {
3310
+ position: 'fixed',
3311
+ top: 0,
3312
+ left: 0,
3313
+ visibility: 'hidden',
3314
+ pointerEvents: 'none',
3315
+ zIndex: 1050,
3316
+ transform: 'none',
3317
+ };
3318
+ const menuItems = (jsxRuntime.jsx("ul", { ref: menuRef, role: "menu", className: classNames('dropdown-menu p-2', { show: open }, classNameMenu), style: menuStyle, children: actions.map((action, index) => {
3045
3319
  if (action.isDivider) {
3046
3320
  return jsxRuntime.jsx("hr", { className: "dropdown-divider" }, index);
3047
3321
  }
@@ -3060,10 +3334,7 @@ function DDropdown({ actions, dropdownToggle, className, classNameMenu, asPortal
3060
3334
  }
3061
3335
  }, disabled: action.disabled, children: [action.icon && jsxRuntime.jsx(DIcon, { icon: action.icon, className: "me-2", size: "1rem" }), action.label] })) }, index));
3062
3336
  }) }));
3063
- if (asPortal) {
3064
- return (jsxRuntime.jsxs("div", { ref: toggleRef, className: classNames('dropdown', className), children: [ToggleElement, open && menuCoords && reactDom.createPortal(menuItems, document.body)] }));
3065
- }
3066
- return (jsxRuntime.jsxs("div", { className: classNames(`dropdown drop-${resolvedInlinePosition}`, className), ref: dropdownRef, children: [ToggleElement, menuItems] }));
3337
+ return (jsxRuntime.jsxs("div", { ref: toggleRef, className: classNames(`dropdown drop-${resolvedSide} d-inline-block`, className), children: [ToggleElement, open && (asPortal ? reactDom.createPortal(menuItems, document.body) : menuItems)] }));
3067
3338
  }
3068
3339
 
3069
3340
  function useScreenshot() {
@@ -3093,13 +3364,18 @@ function useScreenshot() {
3093
3364
 
3094
3365
  function useScreenshotDownload() {
3095
3366
  const { clipRef, takeBlob } = useScreenshot();
3096
- const download = React.useCallback(async () => {
3097
- const blob = await takeBlob();
3367
+ const download = React.useCallback(async (fileName = 'voucher') => {
3368
+ const blob = await takeBlob('image/jpeg');
3098
3369
  const url = window.URL.createObjectURL(blob);
3099
3370
  const link = window.document.createElement('a');
3100
3371
  link.style.display = 'none';
3101
3372
  link.href = url;
3102
- link.download = 'voucher.jpg';
3373
+ // Fall back to the default base name when the caller passes a blank/whitespace value,
3374
+ // so the file is never named just ".jpg".
3375
+ const safeFileName = fileName.trim() || 'voucher';
3376
+ // The extension is always appended by the hook to guarantee a valid image file,
3377
+ // regardless of what fileName the caller passes (e.g. "receipt.png" -> "receipt.png.jpg").
3378
+ link.download = `${safeFileName}.jpg`;
3103
3379
  document.body.appendChild(link);
3104
3380
  link.click();
3105
3381
  document.body.removeChild(link);
@@ -3113,11 +3389,15 @@ function useScreenshotDownload() {
3113
3389
 
3114
3390
  function useScreenshotWebShare() {
3115
3391
  const { takeBlob, clipRef } = useScreenshot();
3116
- const share = React.useCallback(async () => {
3117
- const blob = await takeBlob();
3118
- const image = new File([blob], 'voucher.jpeg', { type: 'image/jpeg' });
3119
- if (!navigator.canShare
3120
- && (navigator.canShare && !navigator.canShare({ files: [image] }))) {
3392
+ const share = React.useCallback(async (fileName = 'voucher') => {
3393
+ const blob = await takeBlob('image/jpeg');
3394
+ // Fall back to the default base name when the caller passes a blank/whitespace value,
3395
+ // so the file is never named just ".jpeg".
3396
+ const safeFileName = fileName.trim() || 'voucher';
3397
+ // The extension is always appended by the hook to guarantee a valid image file,
3398
+ // regardless of what fileName the caller passes (e.g. "receipt.png" -> "receipt.png.jpeg").
3399
+ const image = new File([blob], `${safeFileName}.jpeg`, { type: 'image/jpeg' });
3400
+ if (!navigator.canShare || !navigator.canShare({ files: [image] })) {
3121
3401
  window.print();
3122
3402
  return;
3123
3403
  }
@@ -3129,11 +3409,11 @@ function useScreenshotWebShare() {
3129
3409
  };
3130
3410
  }
3131
3411
 
3132
- function DVoucher({ amount, amountDetails, icon, title, onError, message, downloadText = 'Download', shareText = 'Share', className, children, }) {
3412
+ function DVoucher({ amount, amountDetails, icon, title, onError, message, downloadText = 'Download', shareText = 'Share', fileName = 'voucher', hideActions = false, className, children, }) {
3133
3413
  const { shareRef, share } = useScreenshotWebShare();
3134
3414
  const { downloadRef, download } = useScreenshotDownload();
3135
3415
  const handleShare = () => {
3136
- share()
3416
+ share(fileName)
3137
3417
  .catch(async (err) => {
3138
3418
  if (onError) {
3139
3419
  await onError(err);
@@ -3144,7 +3424,7 @@ function DVoucher({ amount, amountDetails, icon, title, onError, message, downlo
3144
3424
  });
3145
3425
  };
3146
3426
  const handleDownload = () => {
3147
- download()
3427
+ download(fileName)
3148
3428
  .catch(async (err) => {
3149
3429
  if (onError) {
3150
3430
  await onError(err);
@@ -3172,80 +3452,36 @@ function DVoucher({ amount, amountDetails, icon, title, onError, message, downlo
3172
3452
  return (jsxRuntime.jsx("div", { className: classNames('d-voucher', className), ref: (el) => {
3173
3453
  shareRef.current = el;
3174
3454
  downloadRef.current = el;
3175
- }, children: jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "d-voucher-header", children: [resolvedIconProps && (jsxRuntime.jsx(DIcon, Object.assign({}, resolvedIconProps))), jsxRuntime.jsxs("div", { className: "text-center", children: [jsxRuntime.jsx("h3", { className: "mb-2", children: title }), jsxRuntime.jsx("p", { className: "m-0", children: message })] })] }), amount && (jsxRuntime.jsxs("div", { className: "d-voucher-amount", children: [jsxRuntime.jsx("div", { className: classNames('text-center fw-bold fs-3', amountDetails ? 'mb-1' : 'm-0'), children: amount }), amountDetails] })), jsxRuntime.jsx("hr", { className: "my-4" }), children, jsxRuntime.jsx("hr", { className: "my-4" }), jsxRuntime.jsxs("div", { className: "d-voucher-footer", children: [jsxRuntime.jsx(DButton, { onClick: handleShare, iconStart: "Share2", text: shareText, variant: "outline", size: "sm" }), jsxRuntime.jsx(DButton, { onClick: handleDownload, iconStart: "Download", text: downloadText, variant: "outline", size: "sm" })] })] }) }));
3176
- }
3177
-
3178
- function useCountdown(seconds) {
3179
- const [secondsLeft, setSecondsLeft] = React.useState(seconds);
3180
- const [isActive, setIsActive] = React.useState(true);
3181
- const resetCountdown = React.useCallback((newSeconds = seconds) => {
3182
- setIsActive(false);
3183
- setSecondsLeft(newSeconds);
3184
- }, [seconds]);
3185
- const restartCountdown = React.useCallback(() => {
3186
- resetCountdown(seconds);
3187
- setIsActive(true);
3188
- }, [resetCountdown, seconds]);
3189
- React.useEffect(() => {
3190
- if (!isActive) {
3191
- return () => { };
3192
- }
3193
- const interval = setInterval(() => {
3194
- setSecondsLeft((prevSeconds) => {
3195
- const newSeconds = prevSeconds - 1;
3196
- if (newSeconds <= 0) {
3197
- clearInterval(interval);
3198
- setIsActive(false);
3199
- return 0;
3200
- }
3201
- return newSeconds;
3202
- });
3203
- }, 1000);
3204
- return () => clearInterval(interval);
3205
- }, [isActive]);
3206
- return { secondsLeft, restartCountdown };
3455
+ }, children: jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "d-voucher-header", children: [resolvedIconProps && (jsxRuntime.jsx(DIcon, Object.assign({}, resolvedIconProps))), jsxRuntime.jsxs("div", { className: "d-voucher-title-group", children: [jsxRuntime.jsx("h3", { className: "d-voucher-title", children: title }), jsxRuntime.jsx("p", { className: "d-voucher-message", children: message })] })] }), amount && (jsxRuntime.jsxs("div", { className: "d-voucher-amount", children: [jsxRuntime.jsx("div", { className: classNames('d-voucher-amount-value', { 'd-voucher-amount-value-with-details': !!amountDetails }), children: amount }), amountDetails] })), jsxRuntime.jsx("hr", { className: "d-voucher-divider" }), children, !hideActions && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("hr", { className: "my-4" }), jsxRuntime.jsxs("div", { className: "d-voucher-footer", children: [jsxRuntime.jsx(DButton, { onClick: handleShare, iconStart: "Share2", text: shareText, variant: "outline", size: "sm" }), jsxRuntime.jsx(DButton, { onClick: handleDownload, iconStart: "Download", text: downloadText, variant: "outline", size: "sm" })] })] }))] }) }));
3207
3456
  }
3208
3457
 
3209
3458
  const defaultMessage = (secs) => (secs > 0
3210
3459
  ? `Didn't get any code? Resend in: ${secs}s`
3211
3460
  : "Didn't get any code?");
3212
- function OtpCountdown({ seconds, resendText, message, }) {
3213
- const { secondsLeft, restartCountdown } = useCountdown(seconds);
3214
- return (jsxRuntime.jsxs("div", { className: "d-flex gap-2 align-items-center", children: [jsxRuntime.jsx("p", { className: "mb-0 flex-1", children: message ? message(secondsLeft) : defaultMessage(secondsLeft) }), jsxRuntime.jsx(DButton, { text: resendText, variant: "link", className: "text-nowrap", disabled: secondsLeft > 0, onClick: restartCountdown })] }));
3461
+ function OtpCountdown({ secondsLeft, restartCountdown, resendText, message, }) {
3462
+ return (jsxRuntime.jsxs("div", { className: "d-otp-countdown", children: [jsxRuntime.jsx("p", { className: "d-otp-countdown-text", children: message ? message(secondsLeft) : defaultMessage(secondsLeft) }), jsxRuntime.jsx(DButton, { text: resendText, variant: "link", className: "text-nowrap", disabled: secondsLeft > 0, onClick: restartCountdown })] }));
3215
3463
  }
3216
3464
 
3217
3465
  const TEXT_PROPS = {
3218
3466
  resend: 'Resend',
3219
- resendText: 'Resend code',
3467
+ resendText: 'Resend',
3220
3468
  submit: 'Authorize and continue',
3221
3469
  title: 'We will send you a 6-digit code to your associated phone number so you can continue with your request.',
3470
+ invalidCode: 'Invalid code, please try again.',
3222
3471
  contact: (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { children: "Problems with your digital token? Contact us" }), ' ', jsxRuntime.jsx("a", { href: "https://www.modyo.com", className: "link-primary text-nowrap", target: "_blank", rel: "noreferrer", children: "Contact us" })] })),
3223
3472
  };
3224
3473
  function DOtp({ className, action, isLoading, otpSize = 6, texts = TEXT_PROPS, seconds = 15, }) {
3225
- const [otp, setOtp] = React.useState('');
3226
- const [invalid, setInvalid] = React.useState(false);
3227
- const handler = React.useCallback(async () => {
3228
- if (otp.length < otpSize) {
3229
- setInvalid(true);
3230
- return;
3231
- }
3232
- setInvalid(false);
3233
- await action();
3234
- }, [
3235
- otp.length,
3236
- action,
3237
- otpSize,
3238
- ]);
3239
- return (jsxRuntime.jsxs("div", { className: className, children: [jsxRuntime.jsx("p", { children: texts.title }), jsxRuntime.jsxs("div", { className: "d-flex flex-column gap-6 pb-4 px-3", children: [jsxRuntime.jsxs("div", { className: "d-flex flex-column gap-6", children: [jsxRuntime.jsx(DInputPin, { className: "modal-otp-pin", characters: otpSize, onChange: (e) => setOtp(e), invalid: invalid && otp.length < otpSize, placeholder: "0" }), jsxRuntime.jsx(OtpCountdown, { seconds: seconds, resendText: texts.resend })] }), jsxRuntime.jsx("hr", { className: "m-0" }), jsxRuntime.jsxs("div", { className: "d-flex flex-column flex-lg-row gap-4 align-items-center", children: [jsxRuntime.jsx(DButton, { text: texts.submit, onClick: () => {
3240
- handler().catch((err) => {
3474
+ const { setOtp, invalid, submit, isLoading: isSubmitting, secondsLeft, restartCountdown, } = useOtp({ action, otpSize, seconds });
3475
+ return (jsxRuntime.jsxs("div", { className: className, children: [jsxRuntime.jsx("p", { children: texts.title }), jsxRuntime.jsxs("div", { className: "d-otp d-otp-content", children: [jsxRuntime.jsxs("div", { className: "d-otp-fields", children: [jsxRuntime.jsx(DInputPin, { className: "modal-otp-pin", characters: otpSize, onChange: (e) => setOtp(e), invalid: invalid, hint: invalid ? texts.invalidCode : undefined, placeholder: "0" }), jsxRuntime.jsx(OtpCountdown, { secondsLeft: secondsLeft, restartCountdown: restartCountdown, resendText: texts.resendText })] }), jsxRuntime.jsx("hr", { className: "d-otp-divider" }), jsxRuntime.jsxs("div", { className: "d-otp-footer", children: [jsxRuntime.jsx(DButton, { text: texts.submit, onClick: () => {
3476
+ submit().catch((err) => {
3241
3477
  // eslint-disable-next-line no-console
3242
3478
  console.error('Error in DOtp action:', err);
3243
3479
  });
3244
- }, loading: isLoading }), jsxRuntime.jsx("p", { className: "small ms-lg-auto mb-0", children: texts.contact })] })] })] }));
3480
+ }, loading: isLoading || isSubmitting }), jsxRuntime.jsx("p", { className: "d-otp-contact", children: texts.contact })] })] })] }));
3245
3481
  }
3246
3482
 
3247
3483
  function DefaultErrorBoundary({ resetErrorBoundary }) {
3248
- return (jsxRuntime.jsx(DAlert, { color: "danger", showClose: false, children: jsxRuntime.jsxs("div", { className: "d-flex align-items-center gap-2", children: [jsxRuntime.jsx("span", { children: "An unexpected error occurred." }), jsxRuntime.jsx(DButton, { color: "secondary", variant: "outline", size: "sm", onClick: resetErrorBoundary, children: "Retry" })] }) }));
3484
+ return (jsxRuntime.jsx(DAlert, { color: "danger", showClose: false, children: jsxRuntime.jsxs("div", { className: "d-error-boundary-content", children: [jsxRuntime.jsx("span", { children: "An unexpected error occurred." }), jsxRuntime.jsx(DButton, { color: "secondary", variant: "outline", size: "sm", onClick: resetErrorBoundary, children: "Retry" })] }) }));
3249
3485
  }
3250
3486
 
3251
3487
  function DErrorBoundary({ name, fallback, resetKeys, onReset, onError, children, }) {
@@ -3263,15 +3499,15 @@ function DErrorBoundary({ name, fallback, resetKeys, onReset, onError, children,
3263
3499
  }
3264
3500
 
3265
3501
  function ErrorState({ message, onRetry, retryMessage = 'Retry', color = 'danger', }) {
3266
- return (jsxRuntime.jsxs(DAlert, { color: color, className: "d-flex align-items-center gap-3", children: [jsxRuntime.jsx("div", { className: "flex-grow-1", children: jsxRuntime.jsx("p", { className: "mb-0", children: message !== null && message !== void 0 ? message : 'An unexpected error occurred.' }) }), onRetry && (jsxRuntime.jsx(DButton, { onClick: onRetry, text: retryMessage, variant: "outline", iconStart: "RefreshCw" }))] }));
3502
+ return (jsxRuntime.jsxs(DAlert, { color: color, className: "d-error-state", children: [jsxRuntime.jsx("div", { className: "d-error-state-content", children: jsxRuntime.jsx("p", { className: "d-error-state-message", children: message !== null && message !== void 0 ? message : 'An unexpected error occurred.' }) }), onRetry && (jsxRuntime.jsx(DButton, { onClick: onRetry, text: retryMessage, variant: "outline", iconStart: "RefreshCw" }))] }));
3267
3503
  }
3268
3504
 
3269
3505
  function EmptyState({ message, icon = 'FileText', actionText, onAction, }) {
3270
- return (jsxRuntime.jsxs("div", { className: "d-flex flex-column align-items-center justify-content-center p-5 text-center", children: [jsxRuntime.jsx(DIcon, { icon: icon, size: "3rem", className: "text-secondary mb-3" }), jsxRuntime.jsx("p", { className: "text-secondary mb-3", children: message !== null && message !== void 0 ? message : 'No data available.' }), actionText && onAction && (jsxRuntime.jsx(DButton, { onClick: onAction, text: actionText, variant: "outline" }))] }));
3506
+ return (jsxRuntime.jsxs("div", { className: "d-empty-state", children: [jsxRuntime.jsx(DIcon, { icon: icon, size: "3rem", className: "d-empty-state-icon" }), jsxRuntime.jsx("p", { className: "d-empty-state-message", children: message !== null && message !== void 0 ? message : 'No data available.' }), actionText && onAction && (jsxRuntime.jsx(DButton, { onClick: onAction, text: actionText, variant: "outline" }))] }));
3271
3507
  }
3272
3508
 
3273
3509
  function LoadingState({ ariaLabel = 'Loading...', className }) {
3274
- return (jsxRuntime.jsx("div", { className: `d-flex align-items-center justify-content-center p-4 ${className || ''}`.trim(), "aria-busy": "true", "aria-live": "polite", children: jsxRuntime.jsx("span", { className: "spinner-border", role: "status", "aria-label": ariaLabel }) }));
3510
+ return (jsxRuntime.jsx("div", { className: `d-loading-state ${className || ''}`.trim(), "aria-busy": "true", "aria-live": "polite", children: jsxRuntime.jsx("span", { className: "spinner-border", role: "status", "aria-label": ariaLabel }) }));
3275
3511
  }
3276
3512
 
3277
3513
  function render(renderable) {
@@ -3330,7 +3566,7 @@ function DConfirmModalUI({ entry }) {
3330
3566
  }
3331
3567
  });
3332
3568
  }, [onConfirmAction]);
3333
- return (jsxRuntime.jsxs(DModal, { name: entry.id, size: "lg", className: `confirm-modal ${critical ? 'critical-modal' : ''}`, children: [jsxRuntime.jsx(DModal.Header, { onClose: handleClose, showCloseButton: true, children: jsxRuntime.jsx("h5", { className: "fw-bold", children: title }) }), jsxRuntime.jsxs(DModal.Body, { className: "py-3 px-5", children: [jsxRuntime.jsx("p", { className: "mb-4", children: message }), critical && (jsxRuntime.jsx(ForwardedDInput, { type: "text", label: critical.codeLabel || 'Confirmation code', placeholder: critical.inputPlaceholder, value: confirmationCode, onChange: (value) => setConfirmationCode(value), className: "mb-4", autoFocus: true }))] }), jsxRuntime.jsxs(DModal.Footer, { children: [jsxRuntime.jsx(DButton, { text: cancelLabel, variant: "outline", color: "secondary", onClick: handleClose, disabled: internalLoading }), jsxRuntime.jsx(DButton, { text: confirmLabel, color: confirmColor, onClick: handleConfirmClick, disabled: isConfirmDisabled, loading: internalLoading })] })] }));
3569
+ return (jsxRuntime.jsxs(DModal, { name: entry.id, size: "lg", className: `confirm-modal ${critical ? 'critical-modal' : ''}`, children: [jsxRuntime.jsx(DModal.Header, { onClose: handleClose, showCloseButton: true, children: jsxRuntime.jsx("h5", { className: "fw-bold", children: title }) }), jsxRuntime.jsxs(DModal.Body, { className: "d-confirm-modal-body", children: [jsxRuntime.jsx("p", { className: "d-confirm-modal-message", children: message }), critical && (jsxRuntime.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 }))] }), jsxRuntime.jsxs(DModal.Footer, { children: [jsxRuntime.jsx(DButton, { text: cancelLabel, variant: "outline", color: "secondary", onClick: handleClose, disabled: internalLoading }), jsxRuntime.jsx(DButton, { text: confirmLabel, color: confirmColor, onClick: handleConfirmClick, disabled: isConfirmDisabled, loading: internalLoading })] })] }));
3334
3570
  }
3335
3571
 
3336
3572
  /**
@@ -3468,6 +3704,7 @@ exports.getCssVariable = getCssVariable;
3468
3704
  exports.getQueryString = getQueryString;
3469
3705
  exports.subscribeToMediaQuery = subscribeToMediaQuery;
3470
3706
  exports.useConfirmModal = useConfirmModal;
3707
+ exports.useCountdown = useCountdown;
3471
3708
  exports.useDContext = useDContext;
3472
3709
  exports.useDPortalContext = useDPortalContext;
3473
3710
  exports.useDToast = useDToast;
@@ -3483,7 +3720,11 @@ exports.useMediaBreakpointUpXl = useMediaBreakpointUpXl;
3483
3720
  exports.useMediaBreakpointUpXs = useMediaBreakpointUpXs;
3484
3721
  exports.useMediaBreakpointUpXxl = useMediaBreakpointUpXxl;
3485
3722
  exports.useMediaQuery = useMediaQuery;
3723
+ exports.useOtp = useOtp;
3486
3724
  exports.useProvidedRefOrCreate = useProvidedRefOrCreate;
3725
+ exports.useScreenshot = useScreenshot;
3726
+ exports.useScreenshotDownload = useScreenshotDownload;
3727
+ exports.useScreenshotWebShare = useScreenshotWebShare;
3487
3728
  exports.useStackState = useStackState;
3488
3729
  exports.useTabContext = useTabContext;
3489
3730
  exports.validatePhoneNumber = validatePhoneNumber;