@dynamic-framework/ui-react 2.7.0 → 2.9.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 (58) hide show
  1. package/dist/css/dynamic-ui.css +276 -22
  2. package/dist/css/dynamic-ui.min.css +2 -2
  3. package/dist/index.esm.js +677 -298
  4. package/dist/index.esm.js.map +1 -1
  5. package/dist/index.js +683 -297
  6. package/dist/index.js.map +1 -1
  7. package/dist/types/components/DDropdown/DDropdown.d.ts +10 -2
  8. package/dist/types/components/DInputCheck/DInputCheck.d.ts +1 -0
  9. package/dist/types/components/DInputSwitch/DInputSwitch.d.ts +2 -1
  10. package/dist/types/components/DModal/components/DModalHeader.d.ts +5 -1
  11. package/dist/types/components/DOffcanvas/components/DOffcanvasHeader.d.ts +5 -1
  12. package/dist/types/components/DOtp/DOtp.d.ts +1 -0
  13. package/dist/types/components/DOtp/components/OtpCountdown.d.ts +3 -2
  14. package/dist/types/components/DTooltip/DTooltip.d.ts +38 -3
  15. package/dist/types/components/DVoucher/DVoucher.d.ts +3 -1
  16. package/dist/types/components/DVoucher/hooks/useScreenshotDownload.d.ts +1 -1
  17. package/dist/types/components/DVoucher/hooks/useScreenshotDownload.spec.d.ts +1 -0
  18. package/dist/types/components/DVoucher/hooks/useScreenshotWebShare.d.ts +1 -1
  19. package/dist/types/components/DVoucher/hooks/useScreenshotWebShare.spec.d.ts +1 -0
  20. package/dist/types/components/DVoucher/index.d.ts +3 -0
  21. package/dist/types/components/index.d.ts +1 -1
  22. package/dist/types/hooks/index.d.ts +3 -0
  23. package/dist/types/hooks/tests/useCountdown.spec.d.ts +1 -0
  24. package/dist/types/hooks/useCountdown.d.ts +14 -0
  25. package/dist/types/hooks/useInputCurrency.d.ts +1 -6
  26. package/dist/types/hooks/useOtp.d.ts +98 -0
  27. package/dist/types/utils/buildUrl.d.ts +32 -0
  28. package/dist/types/utils/index.d.ts +2 -0
  29. package/dist/types/utils/sanitizeHref.d.ts +37 -0
  30. package/dist/types/utils/tests/buildUrl.spec.d.ts +1 -0
  31. package/dist/types/utils/tests/sanitizeHref.spec.d.ts +1 -0
  32. package/package.json +9 -6
  33. package/src/style/_shame.scss +1 -5
  34. package/src/style/abstracts/variables/_+import.scss +2 -0
  35. package/src/style/abstracts/variables/_box.scss +7 -0
  36. package/src/style/abstracts/variables/_forms.scss +1 -3
  37. package/src/style/abstracts/variables/_input-currency.scss +1 -0
  38. package/src/style/abstracts/variables/_shadow.scss +12 -1
  39. package/src/style/base/_+import.scss +0 -1
  40. package/src/style/base/_form-check.scss +4 -2
  41. package/src/style/components/_+import.scss +7 -0
  42. package/src/style/components/_d-box-file.scss +5 -0
  43. package/src/style/components/_d-box.scss +5 -5
  44. package/src/style/components/_d-close.scss +1 -0
  45. package/src/style/components/_d-confirm-modal.scss +17 -0
  46. package/src/style/components/_d-data-state-wrapper.scss +47 -0
  47. package/src/style/components/_d-datepicker.scss +11 -0
  48. package/src/style/components/_d-error-boundary.scss +7 -0
  49. package/src/style/components/_d-input-currency.scss +5 -0
  50. package/src/style/components/_d-otp.scss +54 -0
  51. package/src/style/components/_d-password-strength-meter.scss +76 -0
  52. package/src/style/components/_d-popover.scss +4 -0
  53. package/src/style/components/_d-stepper.scss +8 -0
  54. package/src/style/components/_d-tabs.scss +18 -0
  55. package/src/style/components/_d-voucher.scss +30 -0
  56. package/dist/types/components/DOtp/DOtpspec.d.ts +0 -0
  57. package/dist/types/components/DOtp/hooks/useCountdown.d.ts +0 -4
  58. package/src/style/base/_form-text.scss +0 -12
@@ -8,7 +8,14 @@ export type DropdownAction = {
8
8
  isDivider?: boolean;
9
9
  label: string;
10
10
  };
11
- type DropdownPlacement = 'auto' | 'down' | 'up' | 'start' | 'end';
11
+ type ResolvedSide = 'down' | 'up' | 'start' | 'end';
12
+ type DropdownPlacement = 'auto' | ResolvedSide;
13
+ /**
14
+ * Horizontal alignment of the menu relative to the toggle, applied only when
15
+ * the resolved placement is vertical (`down`/`up`) — mirrors Bootstrap's
16
+ * `dropdown-menu-end`/`dropdown-center` menu alignment options.
17
+ */
18
+ export type DropdownAlignment = 'start' | 'end' | 'center';
12
19
  type Props = {
13
20
  actions: DropdownAction[];
14
21
  dropdownToggle?: ((props: {
@@ -19,6 +26,7 @@ type Props = {
19
26
  classNameMenu?: string;
20
27
  asPortal?: boolean;
21
28
  placement?: DropdownPlacement;
29
+ alignment?: DropdownAlignment;
22
30
  };
23
- export default function DDropdown({ actions, dropdownToggle, className, classNameMenu, asPortal, placement, }: Props): import("react").JSX.Element;
31
+ export default function DDropdown({ actions, dropdownToggle, className, classNameMenu, asPortal, placement, alignment, }: Props): import("react").JSX.Element;
24
32
  export {};
@@ -12,6 +12,7 @@ type Props = ComponentPropsWithoutRef<'input'> & BaseProps & {
12
12
  invalid?: boolean;
13
13
  valid?: boolean;
14
14
  hint?: string;
15
+ /** Only applies when `type` is `checkbox`; ignored for `radio`. */
15
16
  indeterminate?: boolean;
16
17
  value?: string;
17
18
  onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
@@ -10,8 +10,9 @@ type Props = BaseProps & {
10
10
  inputClassName?: string;
11
11
  invalid?: boolean;
12
12
  valid?: boolean;
13
+ hint?: string;
13
14
  readonly?: boolean;
14
15
  onChange?: (isChecked: boolean) => void;
15
16
  };
16
- export default function DInputSwitch({ id: idProp, label, ariaLabel, name, checked, disabled, invalid, valid, readonly, className, style, dataAttributes, inputClassName, onChange, }: Props): import("react").JSX.Element;
17
+ export default function DInputSwitch({ id: idProp, label, ariaLabel, name, checked, disabled, invalid, valid, hint, readonly, className, style, dataAttributes, inputClassName, onChange, }: Props): import("react").JSX.Element;
17
18
  export {};
@@ -3,8 +3,12 @@ import type { BaseProps, FamilyIconProps } from '../../interface';
3
3
  type Props = BaseProps & FamilyIconProps & PropsWithChildren<{
4
4
  showCloseButton?: boolean;
5
5
  icon?: string;
6
+ iconMaterialStyle?: boolean;
7
+ /**
8
+ * @deprecated Use `iconMaterialStyle` instead. It will be removed in a future major version.
9
+ */
6
10
  materialStyle?: boolean;
7
11
  onClose?: () => void;
8
12
  }>;
9
- export default function DModalHeader({ showCloseButton, onClose, children, className, style, iconFamilyClass, iconFamilyPrefix, icon: iconProp, materialStyle, }: Props): import("react").JSX.Element;
13
+ export default function DModalHeader({ showCloseButton, onClose, children, className, style, iconFamilyClass, iconFamilyPrefix, icon: iconProp, iconMaterialStyle, materialStyle: materialStyleProp, }: Props): import("react").JSX.Element;
10
14
  export {};
@@ -3,8 +3,12 @@ import type { BaseProps, FamilyIconProps } from '../../interface';
3
3
  type Props = BaseProps & FamilyIconProps & PropsWithChildren<{
4
4
  showCloseButton?: boolean;
5
5
  icon?: string;
6
+ iconMaterialStyle?: boolean;
7
+ /**
8
+ * @deprecated Use `iconMaterialStyle` instead. It will be removed in a future major version.
9
+ */
6
10
  materialStyle?: boolean;
7
11
  onClose?: () => void;
8
12
  }>;
9
- export default function DOffcanvasHeader({ showCloseButton, onClose, children, className, style, iconFamilyClass, iconFamilyPrefix, icon: iconProp, materialStyle, }: Props): import("react").JSX.Element;
13
+ export default function DOffcanvasHeader({ showCloseButton, onClose, children, className, style, iconFamilyClass, iconFamilyPrefix, icon: iconProp, iconMaterialStyle, materialStyle: materialStyleProp, }: Props): import("react").JSX.Element;
10
14
  export {};
@@ -11,6 +11,7 @@ export type PropsOtp = PropsWithChildren<{
11
11
  title?: string;
12
12
  contact?: string | React.ReactNode;
13
13
  resendText?: string;
14
+ invalidCode?: string;
14
15
  };
15
16
  }>;
16
17
  export default function DOtp({ className, action, isLoading, otpSize, texts, seconds, }: PropsOtp): import("react").JSX.Element;
@@ -1,8 +1,9 @@
1
1
  /// <reference types="react" />
2
2
  type Props = {
3
- seconds: number;
3
+ secondsLeft: number;
4
+ restartCountdown: () => void;
4
5
  resendText?: string;
5
6
  message?: (secondsLeft: number) => string;
6
7
  };
7
- export default function OtpCountdown({ seconds, resendText, message, }: Props): import("react").JSX.Element;
8
+ export default function OtpCountdown({ secondsLeft, restartCountdown, resendText, message, }: Props): import("react").JSX.Element;
8
9
  export {};
@@ -1,16 +1,51 @@
1
- import type { PropsWithChildren, ReactNode } from 'react';
1
+ import type { ReactNode } from 'react';
2
2
  import type { Placement } from '@floating-ui/react';
3
3
  import type { BaseProps, ComponentSize } from '../interface';
4
- export type Props = BaseProps & PropsWithChildren<{
4
+ export type Props = BaseProps & {
5
+ /** CSS class applied to the wrapper element that surrounds `Component` (the trigger). */
5
6
  childrenClassName?: string;
7
+ /**
8
+ * Distance in pixels between the trigger and the floating tooltip.
9
+ * Defaults to arrow height + gap.
10
+ */
6
11
  offSet?: number;
12
+ /** Virtual padding, in pixels, used to keep the tooltip within the viewport boundaries. */
7
13
  padding?: number;
14
+ /** When true, the tooltip opens when the pointer hovers over the trigger element. */
8
15
  withHover?: boolean;
16
+ /**
17
+ * When true, the tooltip opens when the trigger element receives keyboard focus
18
+ * (recommended for accessibility).
19
+ */
9
20
  withFocus?: boolean;
21
+ /** When true, the tooltip toggles open/closed when the trigger element is clicked. */
10
22
  withClick?: boolean;
23
+ /** Controls the initial open state of the tooltip. */
11
24
  open?: boolean;
25
+ /** Size of the tooltip content. Omit for the default size. */
12
26
  size?: ComponentSize;
27
+ /**
28
+ * Preferred position of the tooltip relative to the trigger element.
29
+ * Flips automatically if there isn't enough space.
30
+ */
13
31
  placement?: Placement;
32
+ /**
33
+ * Element that triggers the tooltip. It is rendered inside a wrapper `<div>`
34
+ * that holds the Floating UI reference and the interaction handlers, so the
35
+ * trigger area is that wrapper rather than this node itself. Style it with
36
+ * `childrenClassName`.
37
+ */
14
38
  Component: ReactNode;
15
- }>;
39
+ /**
40
+ * Floating content rendered inside the tooltip body. Keep it short and
41
+ * non-critical: the tooltip is only visible while it is open.
42
+ */
43
+ children?: ReactNode;
44
+ };
45
+ /**
46
+ * Tooltip is an abstraction component that wraps any element (`Component`) and displays
47
+ * floating contextual content (`children`) positioned relative to it using Floating UI.
48
+ * Use it to add short, non-critical hints or descriptions triggered by hover, focus, or
49
+ * click, without altering the surrounding layout.
50
+ */
16
51
  export default function DTooltip({ className, childrenClassName, style, offSet, padding, withFocus, withClick, withHover, open, placement, size, Component, children, }: Props): import("react").JSX.Element;
@@ -9,7 +9,9 @@ type Props = PropsWithChildren<{
9
9
  title: string;
10
10
  downloadText?: string;
11
11
  shareText?: string;
12
+ fileName?: string;
13
+ hideActions?: boolean;
12
14
  onError?: (err: Error) => Promise<void> | void;
13
15
  }>;
14
- export default function DVoucher({ amount, amountDetails, icon, title, onError, message, downloadText, shareText, className, children, }: Props): import("react").JSX.Element;
16
+ export default function DVoucher({ amount, amountDetails, icon, title, onError, message, downloadText, shareText, fileName, hideActions, className, children, }: Props): import("react").JSX.Element;
15
17
  export {};
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
2
  export default function useScreenshotDownload(): {
3
- download: () => Promise<void>;
3
+ download: (fileName?: string) => Promise<void>;
4
4
  downloadRef: import("react").RefObject<HTMLDivElement | null>;
5
5
  };
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
2
  export default function useScreenshotWebShare(): {
3
- share: () => Promise<void>;
3
+ share: (fileName?: string) => Promise<void>;
4
4
  shareRef: import("react").RefObject<HTMLDivElement | null>;
5
5
  };
@@ -1,2 +1,5 @@
1
1
  import DVoucher from './DVoucher';
2
+ export { default as useScreenshot } from './hooks/useScreenshot';
3
+ export { default as useScreenshotDownload } from './hooks/useScreenshotDownload';
4
+ export { default as useScreenshotWebShare } from './hooks/useScreenshotWebShare';
2
5
  export default DVoucher;
@@ -50,7 +50,7 @@ export type { DToastContainerProps, DToastOptions, ToastData } from './DToastCon
50
50
  export { default as DInputPhone } from './DInputPhone';
51
51
  export { default as DCreditCard } from './DCreditCard';
52
52
  export { default as DDropdown } from './DDropdown';
53
- export { default as DVoucher } from './DVoucher';
53
+ export { default as DVoucher, useScreenshot, useScreenshotDownload, useScreenshotWebShare, } from './DVoucher';
54
54
  export { default as DOtp } from './DOtp';
55
55
  export { default as DErrorBoundary, useErrorBoundary, type FallbackProps, getErrorMessage, } from './DErrorBoundary';
56
56
  export { default as DDataStateWrapper, EmptyState, ErrorState, LoadingState, } from './DDataStateWrapper';
@@ -9,3 +9,6 @@ export { default as useMediaQuery } from './useMediaQuery';
9
9
  export { default as useConfirmModal } from './useConfirmModal';
10
10
  export type { UseConfirmModalConfig, UseConfirmModalReturn, ConfirmModalColor, CriticalConfirmConfig, } from './useConfirmModal';
11
11
  export { useMediaBreakpointUpXs, useMediaBreakpointUpSm, useMediaBreakpointUpMd, useMediaBreakpointUpLg, useMediaBreakpointUpXl, useMediaBreakpointUpXxl, } from './useMediaBreakpointUp';
12
+ export { default as useCountdown } from './useCountdown';
13
+ export { default as useOtp } from './useOtp';
14
+ export type { UseOtpConfig, UseOtpReturn } from './useOtp';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Generic seconds-based countdown hook.
3
+ *
4
+ * Starts counting down from `seconds` to `0` and exposes `restartCountdown`
5
+ * to reset and restart the timer (e.g. for "resend code in Xs" patterns,
6
+ * session-expiry banners, or any other cooldown/throttle UI).
7
+ *
8
+ * @example
9
+ * const { secondsLeft, restartCountdown } = useCountdown(30);
10
+ */
11
+ export default function useCountdown(seconds: number): {
12
+ secondsLeft: number;
13
+ restartCountdown: () => void;
14
+ };
@@ -1,15 +1,10 @@
1
1
  import type { RefObject, ForwardedRef, FocusEvent } from 'react';
2
2
  import type { Options } from 'currency.js';
3
- import type { CustomStyles } from '../components/interface';
4
- export default function useInputCurrency(currencyOptions: Options, value?: number, onFocus?: (event: FocusEvent<HTMLInputElement>) => void, onChange?: (value?: number) => void, onBlur?: (event: FocusEvent<HTMLInputElement>) => void, ref?: ForwardedRef<HTMLInputElement>): {
3
+ export default function useInputCurrency(currencyOptions: Options, value?: number, onFocus?: (event: FocusEvent<HTMLInputElement>) => void, onChange?: (value?: number) => void, onBlur?: (event: FocusEvent<HTMLInputElement>) => void, ref?: ForwardedRef<HTMLInputElement>, minValue?: number, maxValue?: number): {
5
4
  inputRef: RefObject<HTMLInputElement | null>;
6
5
  innerValue: string;
7
6
  innerType: string;
8
7
  handleOnFocus: (event: FocusEvent<HTMLInputElement>) => void;
9
8
  handleOnChange: (newValue?: string) => void;
10
9
  handleOnBlur: (event: FocusEvent<HTMLInputElement>) => void;
11
- generateStyleVariables: CustomStyles;
12
- generateSymbolStyleVariables: {
13
- color: string;
14
- };
15
10
  };
@@ -0,0 +1,98 @@
1
+ export type UseOtpConfig = {
2
+ /** Called when the OTP reaches the expected length and `submit()` is invoked. */
3
+ action: () => Promise<void> | void;
4
+ /** Expected number of digits/characters for a valid OTP. Defaults to `6`. */
5
+ otpSize?: number;
6
+ /** Initial countdown (in seconds) before resend becomes available. Defaults to `15`. */
7
+ seconds?: number;
8
+ };
9
+ export type UseOtpReturn = {
10
+ /** Current OTP value. */
11
+ otp: string;
12
+ /**
13
+ * Updates the OTP value (wire this to your custom pin input's `onChange`).
14
+ * Also clears `invalid`/`error` as soon as the value actually changes, so
15
+ * the error state disappears once the user edits the code (a no-op call
16
+ * with the same value, e.g. from an uncontrolled input re-notifying its
17
+ * current value, does not reset `invalid`/`error`).
18
+ */
19
+ setOtp: (value: string) => void;
20
+ /**
21
+ * `true` when `submit()` is called with an OTP shorter than `otpSize`, or
22
+ * when `action` throws/rejects (e.g. the backend rejected the entered
23
+ * code as incorrect).
24
+ */
25
+ invalid: boolean;
26
+ /**
27
+ * The value thrown/rejected by `action`, or `null` otherwise. Use it to
28
+ * render a specific message for a wrong code, as opposed to a too-short
29
+ * one.
30
+ */
31
+ error: unknown;
32
+ /**
33
+ * Validates the OTP length; if valid, calls `action`. If `action`
34
+ * rejects, the returned promise rejects too (so callers can still
35
+ * `.catch()` it for logging), and `invalid`/`error` are set beforehand.
36
+ */
37
+ submit: () => Promise<void>;
38
+ /**
39
+ * `true` while `action` is being awaited inside `submit()` (i.e. the OTP
40
+ * passed the length check and the async action hasn't settled yet). Wire
41
+ * it to your submit button's loading/disabled state. It's `false` while
42
+ * idle and immediately after `action` settles (whether it resolves or
43
+ * rejects), and it's never set to `true` for the too-short/length
44
+ * validation failure since `action` isn't called in that case.
45
+ */
46
+ isLoading: boolean;
47
+ /** Seconds remaining until resend is available. */
48
+ secondsLeft: number;
49
+ /** Restarts the resend countdown (call it from your "Resend" action). */
50
+ restartCountdown: () => void;
51
+ };
52
+ /**
53
+ * Headless hook exposing the same OTP logic used internally by `DOtp`
54
+ * (code state, length + correctness validation, submit action and resend
55
+ * countdown), without any bundled markup/styles.
56
+ *
57
+ * Use this when the default `DOtp` component doesn't match your design and
58
+ * you need to build a fully custom UI while reusing the same behavior.
59
+ *
60
+ * Internally composes `useCountdown` for the resend timer and re-exposes
61
+ * `secondsLeft`/`restartCountdown`, so there is no need to call
62
+ * `useCountdown` separately.
63
+ *
64
+ * `invalid` becomes `true` both when the OTP is shorter than `otpSize` and
65
+ * when `action` throws/rejects (e.g. the backend rejected the entered code
66
+ * as incorrect). In the latter case, the thrown value is exposed as `error`
67
+ * so custom UIs can render an appropriate message (e.g. "Invalid code" vs.
68
+ * "Enter all digits"). `invalid`/`error` are cleared automatically as soon
69
+ * as the user edits the code via `setOtp`.
70
+ *
71
+ * @example
72
+ * function CustomOtp() {
73
+ * const {
74
+ * otp,
75
+ * setOtp,
76
+ * invalid,
77
+ * error,
78
+ * submit,
79
+ * isLoading,
80
+ * secondsLeft,
81
+ * restartCountdown,
82
+ * } = useOtp({ action: async () => verifyOtp(otp), otpSize: 6, seconds: 15 });
83
+ *
84
+ * return (
85
+ * <>
86
+ * <MyCustomPinInput value={otp} onChange={setOtp} invalid={invalid} />
87
+ * {invalid && <span>{error ? 'Invalid code' : 'Enter all digits'}</span>}
88
+ * <button disabled={isLoading} onClick={() => { submit().catch(() => {}); }}>
89
+ * {isLoading ? 'Submitting...' : 'Submit'}
90
+ * </button>
91
+ * <button disabled={secondsLeft > 0} onClick={restartCountdown}>
92
+ * Resend {secondsLeft > 0 ? `(${secondsLeft}s)` : ''}
93
+ * </button>
94
+ * </>
95
+ * );
96
+ * }
97
+ */
98
+ export default function useOtp({ action, otpSize, seconds, }: UseOtpConfig): UseOtpReturn;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Builds an internal URL from a `path` and a set of query `params`, normalizing leading
3
+ * separators to prevent scheme-relative URLs (e.g. `//accounts` and `/\accounts` both become
4
+ * `/accounts`).
5
+ *
6
+ * The `path` is trimmed and every tab, line feed and carriage return is removed before
7
+ * building the result, because the URL parser ignores those characters too and they would
8
+ * otherwise hide a scheme-relative prefix (`/<TAB>/evil.com` resolves to `//evil.com`).
9
+ *
10
+ * Handles the special case where `path` is the literal string `'#'`, which happens when
11
+ * a Liquid template variable used as a link target is left unconfigured; in that case the
12
+ * params are ignored and `'#'` is returned as-is instead of building a broken URL.
13
+ *
14
+ * @param path - The path to build the URL from. Leading separators are stripped before
15
+ * building the result (`/accounts`, `//accounts` and `\accounts` are all treated as `accounts`).
16
+ * @param params - Key/value pairs serialized as the query string.
17
+ * @returns The resulting URL:
18
+ * - `'#'` when `path` normalizes to `'#'` (`'#'`, `'/#'`), ignoring the params.
19
+ * - `'/'` when `path` is empty and there are no params.
20
+ * - `'/?<query>'` when `path` is empty and there are params.
21
+ * - `'<path>'` or `'<path>?<query>'` when `path` starts with `#`, since a fragment must not be
22
+ * prefixed with `/` (it would navigate away from the current document).
23
+ * - `'/<path>'` or `'/<path>?<query>'` otherwise.
24
+ *
25
+ * @example
26
+ * buildUrl('accounts', { account_id: '42' }); // '/accounts?account_id=42'
27
+ * buildUrl('//accounts', {}); // '/accounts'
28
+ * buildUrl('#', { account_id: '42' }); // '#'
29
+ * buildUrl('#accounts', { account_id: '42' }); // '#accounts?account_id=42'
30
+ * buildUrl('', { tab: 'movimientos' }); // '/?tab=movimientos'
31
+ */
32
+ export default function buildUrl(path: string, params: Record<string, string | number>): string;
@@ -4,4 +4,6 @@ export { default as getQueryString } from './getQueryString';
4
4
  export { default as changeQueryString } from './changeQueryString';
5
5
  export { default as getCssVariable } from './getCssVariable';
6
6
  export { default as validatePhoneNumber } from './validatePhoneNumber';
7
+ export { default as sanitizeHref } from './sanitizeHref';
8
+ export { default as buildUrl } from './buildUrl';
7
9
  export * from './mediaQuery';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Validates a `href` value and returns it only if it is safe to render as a link,
3
+ * preventing XSS through unsafe URL schemes and scheme-relative URLs.
4
+ *
5
+ * Before validating, the value is trimmed and every tab, line feed and carriage return is
6
+ * removed, because the URL parser ignores those characters too and they would otherwise
7
+ * hide an unsafe value from the checks below.
8
+ *
9
+ * A `href` is considered safe when it is either:
10
+ * - An absolute path starting with a single separator (e.g. `/accounts/42`). A leading `\`
11
+ * is normalized to `/` in the returned value, since that is how the browser resolves it.
12
+ * - A URL using one of the allowed schemes: `http:`, `https:`, `mailto:`, `tel:`.
13
+ *
14
+ * A `href` is rejected (returns `undefined`) when it is:
15
+ * - Empty or made only of whitespace/ignored characters (`''`, `' '`).
16
+ * - Scheme-relative (starts with two separators, e.g. `//evil.com`, `/\evil.com`,
17
+ * `/<TAB>/evil.com`), since the browser would resolve it using the current protocol and it
18
+ * can be used to redirect to an attacker-controlled host.
19
+ * - Using a disallowed/dangerous scheme, such as `javascript:` or `data:`
20
+ * (e.g. `javascript:alert(1)`, `data:text/html,<script>...</script>`).
21
+ * - Not parseable as an absolute URL and not an absolute path, such as bare fragments
22
+ * (`#anchor`) or scheme-less relative paths (`relative/path`). These cannot be validated
23
+ * against `SAFE_SCHEMES`, so they are rejected rather than assumed safe.
24
+ *
25
+ * @param href - The raw URL or path to validate, typically coming from user input,
26
+ * a CMS, or an external API.
27
+ * @returns The sanitized `href` when safe, `undefined` otherwise.
28
+ *
29
+ * @example
30
+ * sanitizeHref('/accounts/42'); // '/accounts/42'
31
+ * sanitizeHref('https://example.com'); // 'https://example.com'
32
+ * sanitizeHref('mailto:test@example.com'); // 'mailto:test@example.com'
33
+ * sanitizeHref('//evil.com'); // undefined
34
+ * sanitizeHref('/\\evil.com'); // undefined
35
+ * sanitizeHref('javascript:alert(1)'); // undefined
36
+ */
37
+ export default function sanitizeHref(href: string): string | undefined;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "sideEffects": [
4
4
  "*.css"
5
5
  ],
6
- "version": "2.7.0",
6
+ "version": "2.9.0",
7
7
  "description": "React Dynamic Framework",
8
8
  "license": "https://github.com/dynamic-framework/dynamic-ui/blob/master/LICENSE.md",
9
9
  "repository": {
@@ -40,9 +40,10 @@
40
40
  }
41
41
  ],
42
42
  "scripts": {
43
- "test": "jest",
44
- "test:watch": "jest --watch",
45
- "test:coverage": "jest --coverage",
43
+ "test": "jest --testPathPatterns=\\.spec\\.tsx?$",
44
+ "test:watch": "jest --testPathPatterns=\\.spec\\.tsx?$ --watch",
45
+ "test:coverage": "jest --testPathPatterns=\\.spec\\.tsx?$ --coverage",
46
+ "test:a11y": "jest --testPathPatterns=\\.a11y\\.test\\.tsx$",
46
47
  "test:storybook": "test-storybook",
47
48
  "build": "npm run clean && npm run build:react && npm run build:scss && npm run cp",
48
49
  "build:react": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.mjs",
@@ -80,7 +81,8 @@
80
81
  "prepare": "husky",
81
82
  "commitlint": "commitlint --edit",
82
83
  "verify": "node scripts/verify.js",
83
- "axe": "npx tsx scripts/generate-a11y-report.ts"
84
+ "axe": "npx tsx scripts/generate-a11y-report.ts",
85
+ "axe:browser": "npx tsx scripts/generate-a11y-browser-warnings.ts"
84
86
  },
85
87
  "engines": {
86
88
  "node": ">=22.0.0"
@@ -130,6 +132,7 @@
130
132
  "@testing-library/user-event": "^14.6.1",
131
133
  "@types/google-libphonenumber": "~7.4.30",
132
134
  "@types/jest": "~29.5.12",
135
+ "@types/jest-axe": "^3.5.9",
133
136
  "@types/node": "^22.0.0",
134
137
  "@types/react": "^19.2.0",
135
138
  "@types/react-dom": "^19.2.0",
@@ -157,7 +160,7 @@
157
160
  "jest-axe": "^10.0.0",
158
161
  "jest-environment-jsdom": "^30.3.0",
159
162
  "lint-staged": "^15.2.10",
160
- "playwright": "^1.58.2",
163
+ "playwright": "1.61.1",
161
164
  "postcss-cli": "~10.1.0",
162
165
  "react": "~19.2.1",
163
166
  "react-docgen-typescript": "^2.4.0",
@@ -4,16 +4,12 @@
4
4
  flex: 1;
5
5
  }
6
6
 
7
+ // Deprecated by bootstrap 5.3, but still used in some components
7
8
  .text-muted {
8
9
  --#{$prefix}text-muted: var(--#{$prefix}gray-500);
9
10
  color: var(--#{$prefix}text-muted) !important;
10
11
  }
11
12
 
12
- .form-text {
13
- --#{$prefix}form-text: var(--#{$prefix}gray-500);
14
- color: var(--#{$prefix}form-text) !important;
15
- }
16
-
17
13
  .spinner-border {
18
14
  margin-top: 4px;
19
15
  margin-bottom: 4px;
@@ -91,7 +91,9 @@ $input-btn-border-width: var(--#{$prefix}border-width) !default;
91
91
  @import "cursors";
92
92
  @import "chips";
93
93
  @import "collapse-icon-text";
94
+ @import "box";
94
95
  @import "box-file";
95
96
  @import "datepicker";
96
97
  @import "input-phone";
98
+ @import "input-currency";
97
99
  // end custom
@@ -0,0 +1,7 @@
1
+ // Box (DBox)
2
+
3
+ $box-component-bg: var(--#{$prefix}white) !default;
4
+ $box-component-border: 0 !default;
5
+ $box-component-border-radius: var(--#{$prefix}border-radius) !default;
6
+ $box-component-padding: var(--#{$prefix}ref-spacer-4) !default;
7
+ $box-component-shadow: var(--#{$prefix}box-shadow-sm) !default;
@@ -1,6 +1,6 @@
1
1
  // scss-docs-start form-text-variables
2
2
  $form-text-margin-top: var(--#{$prefix}ref-spacer-1) !default;
3
- $form-text-color: var(--#{$prefix}gray-400) !default;
3
+ $form-text-color: var(--#{$prefix}gray-500) !default;
4
4
  // scss-docs-end form-text-variables
5
5
 
6
6
  // scss-docs-start form-label-variables
@@ -81,8 +81,6 @@ $input-group-addon-border-color: transparent !default;
81
81
  // custom
82
82
  $input-group-sm-icon-size: $spacer-4 !default;
83
83
  $input-group-lg-icon-size: $spacer-6 !default;
84
- $form-text-padding-y: 0 !default;
85
- $form-text-padding-x: 0 !default;
86
84
  // end custom
87
85
 
88
86
  // scss-docs-start form-select-variables
@@ -0,0 +1 @@
1
+ $input-currency-symbol-color: var(--#{$prefix}secondary) !default;
@@ -1 +1,12 @@
1
- $box-shadow-lg: 0 10px 10px rgba(0, 0, 0, .02) !default;
1
+ // Shadows
2
+ //
3
+ // Bootstrap's shadow scale, declared here explicitly so the elevation ramp is
4
+ // visible in one place and can be retuned quickly. Keep it monotonic:
5
+ // `sm` < default < `lg`, otherwise the `.shadow-*` / `.hover:shadow-*`
6
+ // utilities stop matching what Bootstrap's docs show.
7
+ //
8
+ // scss-docs-start box-shadow-variables
9
+ $box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15) !default;
10
+ $box-shadow-sm: 0 .125rem .25rem rgba(0, 0, 0, .075) !default;
11
+ $box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, .175) !default;
12
+ // scss-docs-end box-shadow-variables
@@ -43,7 +43,6 @@
43
43
  @import "form-control";
44
44
  @import "form-range";
45
45
  @import "list-group";
46
- @import "form-text";
47
46
  @import "input-group";
48
47
  @import "buttons";
49
48
  @import "button-group";
@@ -8,10 +8,12 @@
8
8
  }
9
9
 
10
10
  .form-check {
11
+ // .form-check padding-start only accounts for the input width, the gap between
12
+ // the input and the label comes from .form-check-label's --bs-label-padding-x.
13
+ // The hint must repeat that offset to stay aligned with the label text.
11
14
  .form-text {
12
- --#{$prefix}form-text-padding: 0 var(--#{$prefix}ref-spacer-2);
13
-
14
15
  display: flex;
16
+ padding-inline: var(--#{$prefix}ref-spacer-2);
15
17
  }
16
18
 
17
19
  @each $state, $data in $form-validation-states {
@@ -22,3 +22,10 @@
22
22
  @import "d-table-head";
23
23
  @import "d-input-phone";
24
24
  @import "d-credit-card";
25
+ @import "d-input-currency";
26
+ @import "d-confirm-modal";
27
+ @import "d-data-state-wrapper";
28
+ @import "d-error-boundary";
29
+ @import "d-otp";
30
+ @import "d-password-strength-meter";
31
+ @import "d-stepper";
@@ -69,6 +69,11 @@
69
69
  }
70
70
  }
71
71
 
72
+ .d-box-file-empty-text {
73
+ margin: 0;
74
+ text-align: center;
75
+ }
76
+
72
77
  .d-box-files {
73
78
  --#{$prefix}box-file-list-gap: #{$box-file-list-gap};
74
79
  --#{$prefix}box-file-list-padding: #{$box-file-list-padding};
@@ -1,9 +1,9 @@
1
1
  .d-box {
2
- --#{$prefix}box-component-bg: #{var(--#{$prefix}white)};
3
- --#{$prefix}box-component-border: 0;
4
- --#{$prefix}box-component-border-radius: #{var(--#{$prefix}border-radius)};
5
- --#{$prefix}box-component-padding: #{var(--#{$prefix}ref-spacer-4)};
6
- --#{$prefix}box-component-shadow: #{var(--#{$prefix}box-shadow-lg)};
2
+ --#{$prefix}box-component-bg: #{$box-component-bg};
3
+ --#{$prefix}box-component-border: #{$box-component-border};
4
+ --#{$prefix}box-component-border-radius: #{$box-component-border-radius};
5
+ --#{$prefix}box-component-padding: #{$box-component-padding};
6
+ --#{$prefix}box-component-shadow: #{$box-component-shadow};
7
7
 
8
8
  padding: var(--#{$prefix}box-component-padding);
9
9
  background-color: var(--#{$prefix}box-component-bg);
@@ -1,5 +1,6 @@
1
1
  .d-close {
2
2
  display: flex;
3
+ align-self: center;
3
4
  padding: 0;
4
5
  background: transparent;
5
6
  border: 0;