@aurora-ds/components 1.5.0 → 1.7.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.
package/dist/cjs/index.js CHANGED
@@ -379,8 +379,8 @@ const buildActionButtonRootStyle = (theme, variant, color, focusOptions) => {
379
379
  backgroundColor: intent.main,
380
380
  borderColor: intent.main,
381
381
  color: intent.on,
382
- ':hover:not(:disabled)': { backgroundColor: intent.hover, borderColor: intent.hover },
383
- ':active:not(:disabled)': { backgroundColor: intent.active, borderColor: intent.active },
382
+ ':hover:not(:disabled)': { backgroundColor: intent.hover },
383
+ ':active:not(:disabled)': { backgroundColor: intent.active },
384
384
  }
385
385
  : variant === 'outlined'
386
386
  ? {
@@ -388,7 +388,7 @@ const buildActionButtonRootStyle = (theme, variant, color, focusOptions) => {
388
388
  borderColor: intent.border,
389
389
  color: intent.fg,
390
390
  ':hover:not(:disabled)': { backgroundColor: intent.subtleHover, color: intent.fgHover },
391
- ':active:not(:disabled)': { backgroundColor: intent.subtleActive, borderColor: intent.active, color: intent.active },
391
+ ':active:not(:disabled)': { backgroundColor: intent.subtleActive, color: intent.active },
392
392
  }
393
393
  : {
394
394
  backgroundColor: 'transparent',
@@ -410,7 +410,7 @@ const buildActionButtonRootStyle = (theme, variant, color, focusOptions) => {
410
410
  height: DEFAULT_BUTTON_HEIGHT,
411
411
  cursor: 'pointer',
412
412
  outline: 'none',
413
- transition: `background-color ${theme.transition.normal}, border-color ${theme.transition.normal}, color ${theme.transition.normal}`,
413
+ transition: `background-color ${theme.transition.normal}, color ${theme.transition.normal}`,
414
414
  ...colorVariantStyles,
415
415
  ':focus-visible': getFocusRingStyles(theme, focusOptions),
416
416
  ':disabled': { cursor: 'not-allowed', opacity: theme.opacity.high },
@@ -972,13 +972,13 @@ const LINK_STYLES = theme.createStyles((theme) => ({
972
972
  * In that case the component stays accessible: it gets `role="link"`,
973
973
  * `tabIndex={0}` and keyboard Enter support automatically.
974
974
  *
975
- * @example <Link href='/about'>About</Link>
976
- * @example <Link href='https://example.com' external>External site</Link>
977
- * @example <Link href='/profile' underline='always' startIcon={UserIcon}>Profile</Link>
978
- * @example <Link href='/terms' underline='none'>Terms</Link>
979
- * @example <Link onClick={() => navigate('/about')}>About (SPA)</Link>
975
+ * @example <Link href='/about' label='About' />
976
+ * @example <Link href='https://example.com' label='External site' external />
977
+ * @example <Link href='/profile' underline='always' startIcon={UserIcon} label='Profile' />
978
+ * @example <Link href='/terms' underline='none' label='Terms' />
979
+ * @example <Link onClick={() => navigate('/about')} label='About (SPA)' />
980
980
  */
981
- const Link = ({ ref, underline = 'hover', color = 'default', external = false, disabled = false, startIcon: StartIcon, endIcon: EndIcon, children, className, href, onClick, onKeyDown, ...rest }) => {
981
+ const Link = ({ ref, label, fontSize = 'md', underline = 'hover', color = 'default', external = false, disabled = false, startIcon: StartIcon, endIcon: EndIcon, className, href, onClick, onKeyDown, ...rest }) => {
982
982
  // An <a> without href has no implicit ARIA role and is not focusable.
983
983
  // When used for SPA navigation (onClick only), we restore both behaviours.
984
984
  const hasHref = !!href;
@@ -1004,7 +1004,7 @@ const Link = ({ ref, underline = 'hover', color = 'default', external = false, d
1004
1004
  // With href: the browser handles focusability natively (no tabIndex needed).
1005
1005
  tabIndex: disabled ? -1 : (!hasHref ? 0 : undefined),
1006
1006
  // Without href: <a> has no implicit ARIA role — add role="link" explicitly.
1007
- role: !hasHref ? 'link' : undefined, target: external ? '_blank' : undefined, rel: external ? 'noopener noreferrer' : undefined, onClick: handleClick, onKeyDown: handleKeyDown, ...rest, children: [StartIcon && (jsxRuntime.jsx("span", { className: LINK_STYLES.icon, "aria-hidden": true, children: jsxRuntime.jsx(StartIcon, { width: '1em', height: '1em' }) })), children, EndIcon && (jsxRuntime.jsx("span", { className: LINK_STYLES.icon, "aria-hidden": true, children: jsxRuntime.jsx(EndIcon, { width: '1em', height: '1em' }) }))] }));
1007
+ role: !hasHref ? 'link' : undefined, target: external ? '_blank' : undefined, rel: external ? 'noopener noreferrer' : undefined, onClick: handleClick, onKeyDown: handleKeyDown, ...rest, children: [StartIcon && (jsxRuntime.jsx("span", { className: LINK_STYLES.icon, "aria-hidden": true, children: jsxRuntime.jsx(StartIcon, { width: '1em', height: '1em' }) })), jsxRuntime.jsx(Text, { as: 'span', fontSize: fontSize, children: label }), EndIcon && (jsxRuntime.jsx("span", { className: LINK_STYLES.icon, "aria-hidden": true, children: jsxRuntime.jsx(EndIcon, { width: '1em', height: '1em' }) }))] }));
1008
1008
  };
1009
1009
  Link.displayName = 'Link';
1010
1010
 
@@ -3298,17 +3298,17 @@ const TEXTFIELD_WRAPPER_VARIANTS = theme.createVariants((theme) => {
3298
3298
  return {
3299
3299
  base: {
3300
3300
  display: 'flex',
3301
- alignItems: 'center',
3302
- gap: theme.spacing.sm,
3301
+ // `stretch` lets the inner input fill the full height of the box so
3302
+ // clicking anywhere (including the vertical padding area) hits it.
3303
+ alignItems: 'stretch',
3303
3304
  boxSizing: 'border-box',
3304
3305
  borderWidth: '1px',
3305
3306
  borderStyle: 'solid',
3306
3307
  borderRadius: theme.radius.md,
3307
3308
  backgroundColor: c.surfacePaper,
3309
+ // No focus ring on text inputs: focus is conveyed by the status
3310
+ // border colour change (see the `status` variants below).
3308
3311
  transition: `border-color ${theme.transition.fast}`,
3309
- // Full, always-visible focus ring when the inner input is focused
3310
- // (single source of truth) — complements the status border colour.
3311
- ':focus-within': getFocusRingStyles(theme),
3312
3312
  '&[data-disabled]': {
3313
3313
  opacity: theme.opacity.high,
3314
3314
  backgroundColor: c.disabledMain,
@@ -3317,10 +3317,12 @@ const TEXTFIELD_WRAPPER_VARIANTS = theme.createVariants((theme) => {
3317
3317
  },
3318
3318
  variants: {
3319
3319
  size: {
3320
+ // Horizontal padding lives on the input itself (see TEXTFIELD_INPUT_VARIANTS),
3321
+ // not here, so the whole bordered area is part of the clickable input.
3320
3322
  // Font-size is set on the wrapper so the native input inherits it via `fontSize: 'inherit'`.
3321
- sm: { height: '2rem', paddingLeft: theme.spacing.sm, paddingRight: theme.spacing.xs, fontSize: theme.fontSize.xs },
3322
- md: { height: '2.5rem', paddingLeft: theme.spacing.sm, paddingRight: theme.spacing.xs, fontSize: theme.fontSize.sm },
3323
- lg: { height: '3rem', paddingLeft: theme.spacing.md, paddingRight: theme.spacing.sm, fontSize: theme.fontSize.md },
3323
+ sm: { height: '2rem', fontSize: theme.fontSize.xs },
3324
+ md: { height: '2.5rem', fontSize: theme.fontSize.sm },
3325
+ lg: { height: '3rem', fontSize: theme.fontSize.md },
3324
3326
  },
3325
3327
  status: {
3326
3328
  default: {
@@ -3345,11 +3347,17 @@ const TEXTFIELD_WRAPPER_VARIANTS = theme.createVariants((theme) => {
3345
3347
  defaultVariants: { size: 'md', status: 'default' },
3346
3348
  };
3347
3349
  }, { id: 'textfield-wrapper' });
3348
- const TEXTFIELD_STYLES = theme.createStyles((theme) => ({
3349
- /** Visually hidden native input fills the remaining space inside the wrapper. */
3350
- input: {
3350
+ /**
3351
+ * The native input carries the horizontal padding and fills 100% of the
3352
+ * wrapper height, so the entire bordered box is part of the clickable input
3353
+ * (no dead zone between the text and the border).
3354
+ */
3355
+ const TEXTFIELD_INPUT_VARIANTS = theme.createVariants((theme) => ({
3356
+ base: {
3351
3357
  flex: 1,
3352
3358
  minWidth: 0,
3359
+ height: '100%',
3360
+ boxSizing: 'border-box',
3353
3361
  border: 'none',
3354
3362
  outline: 'none',
3355
3363
  background: 'transparent',
@@ -3357,15 +3365,25 @@ const TEXTFIELD_STYLES = theme.createStyles((theme) => ({
3357
3365
  fontFamily: 'inherit',
3358
3366
  fontSize: 'inherit',
3359
3367
  lineHeight: 'normal',
3360
- padding: '0',
3361
3368
  '&::placeholder': { color: theme.colors.textTertiary },
3362
3369
  '&:disabled': { cursor: 'not-allowed', color: theme.colors.textDisabled },
3363
3370
  },
3371
+ variants: {
3372
+ size: {
3373
+ sm: { paddingLeft: theme.spacing.sm, paddingRight: theme.spacing.sm },
3374
+ md: { paddingLeft: theme.spacing.sm, paddingRight: theme.spacing.sm },
3375
+ lg: { paddingLeft: theme.spacing.md, paddingRight: theme.spacing.md },
3376
+ },
3377
+ },
3378
+ defaultVariants: { size: 'md' },
3379
+ }), { id: 'textfield-input' });
3380
+ const TEXTFIELD_STYLES = theme.createStyles((theme) => ({
3364
3381
  /** Wrapper for the start icon — aligned with the input baseline. Clickable to focus the input. */
3365
3382
  startIconWrap: {
3366
3383
  display: 'flex',
3367
3384
  alignItems: 'center',
3368
3385
  flexShrink: 0,
3386
+ paddingLeft: theme.spacing.sm,
3369
3387
  cursor: 'pointer',
3370
3388
  },
3371
3389
  /** Wrapper for end actions (custom content + optional password toggle). */
@@ -3373,6 +3391,7 @@ const TEXTFIELD_STYLES = theme.createStyles((theme) => ({
3373
3391
  display: 'flex',
3374
3392
  alignItems: 'center',
3375
3393
  flexShrink: 0,
3394
+ paddingRight: theme.spacing.xs,
3376
3395
  gap: theme.spacing['2xs'],
3377
3396
  },
3378
3397
  }), { id: 'textfield-extra' });
@@ -3433,7 +3452,11 @@ const useTextField = ({ id, ref, type, size, endAction, }) => {
3433
3452
  */
3434
3453
  const TextField = ({ ref, label, helperText, size = 'md', status = 'default', startIcon: StartIcon, endAction, type, id, disabled, required, ...rest }) => {
3435
3454
  const { fieldId, helperId, mergedRef, isPassword, showPassword, togglePassword, resolvedType, iconSize, iconButtonSize, hasEndSection, focusInput, } = useTextField({ id, ref, type, size, endAction });
3436
- return (jsxRuntime.jsxs(Stack, { flexDirection: 'column', gap: 'xs', children: [label !== undefined && (jsxRuntime.jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, children: [label, required && (jsxRuntime.jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsxRuntime.jsxs("div", { className: TEXTFIELD_WRAPPER_VARIANTS({ size, status }), "data-disabled": disabled || undefined, children: [StartIcon && (jsxRuntime.jsx("span", { className: TEXTFIELD_STYLES.startIconWrap, onClick: focusInput, "aria-hidden": true, children: jsxRuntime.jsx(Icon, { icon: StartIcon, size: iconSize, strokeColor: 'textSecondary' }) })), jsxRuntime.jsx("input", { ref: mergedRef, id: fieldId, type: resolvedType, disabled: disabled, required: required, "aria-required": required || undefined, "aria-invalid": status === 'error' || undefined, "aria-describedby": helperText !== undefined ? helperId : undefined, "aria-errormessage": status === 'error' && helperText !== undefined ? helperId : undefined, className: TEXTFIELD_STYLES.input, ...rest }), hasEndSection && (jsxRuntime.jsxs("span", { className: TEXTFIELD_STYLES.endActionWrap, children: [endAction, isPassword && (jsxRuntime.jsx(IconButton, { icon: showPassword ? EyeSlashIcon : EyeIcon, ariaLabel: showPassword ? 'Hide password' : 'Show password', variant: 'text', color: 'neutral', size: iconButtonSize, type: 'button', onClick: togglePassword }))] }))] }), helperText !== undefined && (jsxRuntime.jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
3455
+ // Associate the label with the input via `aria-labelledby` (and NOT `htmlFor`)
3456
+ // so the label stays accessible without natively focusing the input on click —
3457
+ // only clicking inside the bordered box should focus the field.
3458
+ const labelId = `${fieldId}-label`;
3459
+ return (jsxRuntime.jsxs(Stack, { flexDirection: 'column', gap: 'xs', children: [label !== undefined && (jsxRuntime.jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', id: labelId, children: [label, required && (jsxRuntime.jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsxRuntime.jsxs("div", { className: TEXTFIELD_WRAPPER_VARIANTS({ size, status }), "data-disabled": disabled || undefined, children: [StartIcon && (jsxRuntime.jsx("span", { className: TEXTFIELD_STYLES.startIconWrap, onClick: focusInput, "aria-hidden": true, children: jsxRuntime.jsx(Icon, { icon: StartIcon, size: iconSize, strokeColor: 'textSecondary' }) })), jsxRuntime.jsx("input", { ref: mergedRef, id: fieldId, type: resolvedType, disabled: disabled, required: required, "aria-required": required || undefined, "aria-invalid": status === 'error' || undefined, "aria-labelledby": label !== undefined ? labelId : undefined, "aria-describedby": helperText !== undefined ? helperId : undefined, "aria-errormessage": status === 'error' && helperText !== undefined ? helperId : undefined, className: TEXTFIELD_INPUT_VARIANTS({ size }), ...rest }), hasEndSection && (jsxRuntime.jsxs("span", { className: TEXTFIELD_STYLES.endActionWrap, children: [endAction, isPassword && (jsxRuntime.jsx(IconButton, { icon: showPassword ? EyeSlashIcon : EyeIcon, ariaLabel: showPassword ? 'Hide password' : 'Show password', variant: 'text', color: 'neutral', size: iconButtonSize, type: 'button', onClick: togglePassword }))] }))] }), helperText !== undefined && (jsxRuntime.jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
3437
3460
  };
3438
3461
  TextField.displayName = 'TextField';
3439
3462
 
@@ -4026,8 +4049,8 @@ const SELECT_TRIGGER_VARIANTS = theme.createVariants((theme) => {
4026
4049
  transition: `border-color ${theme.transition.fast}`,
4027
4050
  outline: 'none',
4028
4051
  fontFamily: 'inherit',
4029
- // Full, always-visible focus ring (single source of truth).
4030
- ':focus-visible': getFocusRingStyles(theme),
4052
+ // No focus ring on the trigger: focus is conveyed by the border
4053
+ // colour change (see the `status` variants below).
4031
4054
  '&[data-open]': {
4032
4055
  borderColor: c.primaryMain,
4033
4056
  },
@@ -4064,6 +4087,9 @@ const SELECT_TRIGGER_VARIANTS = theme.createVariants((theme) => {
4064
4087
  ':hover:not([data-disabled]):not([data-open])': {
4065
4088
  borderColor: c.borderStrong,
4066
4089
  },
4090
+ '&:focus-visible:not([data-open])': {
4091
+ borderColor: c.primaryMain,
4092
+ },
4067
4093
  },
4068
4094
  error: {
4069
4095
  borderColor: c.errorMain,
@@ -5646,7 +5672,13 @@ const DRAWER_STYLES = theme.createStyles((theme) => ({
5646
5672
  * Keep in sync with themeBreakpoints.
5647
5673
  */
5648
5674
  const BREAKPOINTS = {
5649
- sm: 640};
5675
+ xs: 480,
5676
+ sm: 640,
5677
+ md: 768,
5678
+ lg: 1024,
5679
+ xl: 1280,
5680
+ '2xl': 1536,
5681
+ };
5650
5682
  /** Max-width media query strings (max = breakpoint - 1px). */
5651
5683
  const MEDIA_MAX = {
5652
5684
  sm: `max-width: ${BREAKPOINTS.sm - 1}px`};
@@ -6545,6 +6577,13 @@ const AlertContext = React.createContext({
6545
6577
  });
6546
6578
  const useAlertContext = () => React.useContext(AlertContext);
6547
6579
 
6580
+ const VARIANT_DISMISS_COLOR = {
6581
+ default: 'neutral',
6582
+ success: 'success',
6583
+ error: 'error',
6584
+ warning: 'warning',
6585
+ info: 'info',
6586
+ };
6548
6587
  const VARIANT_ICONS = {
6549
6588
  success: AlertSuccessIcon,
6550
6589
  error: AlertErrorIcon,
@@ -6557,7 +6596,7 @@ const isSvgComponent = (value) => typeof value === 'function';
6557
6596
  * Alert title row: renders the variant icon alongside the title text.
6558
6597
  * Must be used inside an `<Alert>` component.
6559
6598
  */
6560
- const AlertTitle = ({ children, icon }) => {
6599
+ const AlertTitle = ({ children, icon, onDismiss }) => {
6561
6600
  const { variant, accentColor } = useAlertContext();
6562
6601
  // Resolve which icon to render:
6563
6602
  // - custom icon prop always takes precedence over the built-in variant icon
@@ -6567,7 +6606,7 @@ const AlertTitle = ({ children, icon }) => {
6567
6606
  const ResolvedIcon = icon && isSvgComponent(icon) ? icon : (icon === undefined ? (builtInIcon ?? null) : null);
6568
6607
  const customNode = icon && !isSvgComponent(icon) ? icon : null;
6569
6608
  const hasIcon = ResolvedIcon !== null || customNode !== null;
6570
- return (jsxRuntime.jsxs(Stack, { flexDirection: 'row', alignItems: 'center', gap: 'sm', children: [hasIcon && (jsxRuntime.jsx(Stack, { flexShrink: 0, alignItems: 'center', color: accentColor, width: '1.25rem', height: '1.25rem', "aria-hidden": true, children: ResolvedIcon ? (jsxRuntime.jsx(ResolvedIcon, { width: 20, height: 20 })) : (customNode) })), jsxRuntime.jsx(Text, { fontWeight: 'semibold', fontSize: 'sm', color: accentColor, children: children })] }));
6609
+ return (jsxRuntime.jsxs(Stack, { flexDirection: 'row', alignItems: 'center', gap: 'sm', justifyContent: onDismiss ? 'space-between' : undefined, children: [jsxRuntime.jsxs(Stack, { flexDirection: 'row', alignItems: 'center', gap: 'sm', children: [hasIcon && (jsxRuntime.jsx(Stack, { flexShrink: 0, alignItems: 'center', color: accentColor, width: '1.25rem', height: '1.25rem', "aria-hidden": true, children: ResolvedIcon ? (jsxRuntime.jsx(ResolvedIcon, { width: 20, height: 20 })) : (customNode) })), jsxRuntime.jsx(Text, { fontWeight: 'semibold', fontSize: 'sm', color: accentColor, children: children })] }), onDismiss && (jsxRuntime.jsx(IconButton, { icon: CloseIcon, ariaLabel: 'Dismiss', variant: 'text', color: VARIANT_DISMISS_COLOR[variant], size: 'sm', onClick: onDismiss }))] }));
6571
6610
  };
6572
6611
  AlertTitle.displayName = 'Alert.Title';
6573
6612
 
@@ -6577,6 +6616,22 @@ AlertTitle.displayName = 'Alert.Title';
6577
6616
  const AlertBody = ({ children }) => (jsxRuntime.jsx(Text, { as: 'p', fontSize: 'sm', color: 'textSecondary', children: children }));
6578
6617
  AlertBody.displayName = 'Alert.Body';
6579
6618
 
6619
+ /**
6620
+ * Alert actions row: renders action elements (buttons, links…) at the bottom of an `<Alert>`.
6621
+ * Must be used inside an `<Alert>` component.
6622
+ *
6623
+ * @example
6624
+ * <Alert variant="error">
6625
+ * <Alert.Title>Something went wrong</Alert.Title>
6626
+ * <Alert.Body>Please try again.</Alert.Body>
6627
+ * <Alert.Actions>
6628
+ * <Button size="sm" variant="outlined" color="error">Retry</Button>
6629
+ * </Alert.Actions>
6630
+ * </Alert>
6631
+ */
6632
+ const AlertActions = ({ children }) => (jsxRuntime.jsx(Stack, { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'end', gap: 'sm', paddingTop: 'xs', children: children }));
6633
+ AlertActions.displayName = 'Alert.Actions';
6634
+
6580
6635
  const VARIANT_TOKENS = {
6581
6636
  default: { backgroundColor: 'surfacePaper', borderColor: 'defaultMain', accentColor: 'defaultActive' },
6582
6637
  success: { backgroundColor: 'successSubtle', borderColor: 'successMain', accentColor: 'successActive' },
@@ -6618,6 +6673,7 @@ AlertBase.displayName = 'Alert';
6618
6673
  const Alert = AlertBase;
6619
6674
  Alert.Title = AlertTitle;
6620
6675
  Alert.Body = AlertBody;
6676
+ Alert.Actions = AlertActions;
6621
6677
 
6622
6678
  const TRANSITION = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
6623
6679
  const DIALOG_STYLES = theme.createStyles((theme) => ({
@@ -6664,12 +6720,18 @@ const DIALOG_STYLES = theme.createStyles((theme) => ({
6664
6720
  transform: 'translateY(0)',
6665
6721
  },
6666
6722
  },
6667
- // Full-height variant on mobile (applied via `fullscreen` prop)
6723
+ // Full-viewport variant (applied via `fullscreen` prop — all screen sizes)
6668
6724
  panelFullscreen: {
6669
- [`@media (${MEDIA_MAX.sm})`]: {
6670
- height: '100dvh',
6671
- maxHeight: 'none',
6672
- },
6725
+ top: 0,
6726
+ left: 0,
6727
+ right: 0,
6728
+ bottom: 0,
6729
+ margin: 0,
6730
+ width: '100%',
6731
+ height: '100dvh',
6732
+ maxWidth: 'none',
6733
+ maxHeight: 'none',
6734
+ borderRadius: 0,
6673
6735
  },
6674
6736
  }), { id: 'dialog' });
6675
6737
 
@@ -6766,6 +6828,114 @@ const Dialog = DialogBase;
6766
6828
  Dialog.Header = DialogHeader;
6767
6829
  Dialog.Body = DialogBody;
6768
6830
 
6831
+ /**
6832
+ * Listens to a CSS media query string and returns whether it currently matches.
6833
+ *
6834
+ * @param query - A valid CSS media query string, e.g. `'(max-width: 639px)'`.
6835
+ * @returns `true` when the media query matches, `false` otherwise.
6836
+ *
6837
+ * @example
6838
+ * const isMobile = useMediaQuery('(max-width: 639px)')
6839
+ */
6840
+ const useMediaQuery = (query) => {
6841
+ const [matches, setMatches] = React.useState(() => {
6842
+ if (typeof window === 'undefined') {
6843
+ return false;
6844
+ }
6845
+ return window.matchMedia(query).matches;
6846
+ });
6847
+ React.useEffect(() => {
6848
+ if (typeof window === 'undefined') {
6849
+ return;
6850
+ }
6851
+ const mediaQueryList = window.matchMedia(query);
6852
+ setMatches(mediaQueryList.matches);
6853
+ const listener = (event) => setMatches(event.matches);
6854
+ mediaQueryList.addEventListener('change', listener);
6855
+ return () => mediaQueryList.removeEventListener('change', listener);
6856
+ }, [query]);
6857
+ return matches;
6858
+ };
6859
+ /**
6860
+ * Returns `true` when the viewport width is **below** the given breakpoint (mobile-first max).
6861
+ *
6862
+ * @param breakpoint - One of the Aurora breakpoints: `xs` | `sm` | `md` | `lg` | `xl` | `2xl`.
6863
+ *
6864
+ * @example
6865
+ * const isMobile = useBreakpointMax('sm') // true when width < 640px
6866
+ */
6867
+ const useBreakpointMax = (breakpoint) => useMediaQuery(`(max-width: ${BREAKPOINTS[breakpoint] - 1}px)`);
6868
+ /**
6869
+ * Returns `true` when the viewport width is **at or above** the given breakpoint.
6870
+ *
6871
+ * @param breakpoint - One of the Aurora breakpoints: `xs` | `sm` | `md` | `lg` | `xl` | `2xl`.
6872
+ *
6873
+ * @example
6874
+ * const isDesktop = useBreakpointMin('md') // true when width >= 768px
6875
+ */
6876
+ const useBreakpointMin = (breakpoint) => useMediaQuery(`(min-width: ${BREAKPOINTS[breakpoint]}px)`);
6877
+
6878
+ /**
6879
+ * Manages keyboard navigation for a listbox-style menu.
6880
+ *
6881
+ * Binds ArrowDown, ArrowUp, Home, End, and Enter using `useKeyPress`.
6882
+ * Automatically skips disabled items and optionally wraps around (loop).
6883
+ * Resets focus when `enabled` toggles.
6884
+ */
6885
+ const useListKeyNav = ({ itemCount, enabled, onSelect, isDisabled, loop = true, initialIndex = 0, }) => {
6886
+ const [focusedIndex, setFocusedIndex] = React.useState(-1);
6887
+ const initialIndexRef = React.useRef(initialIndex);
6888
+ React.useEffect(() => {
6889
+ initialIndexRef.current = initialIndex;
6890
+ }, [initialIndex]);
6891
+ React.useEffect(() => {
6892
+ setFocusedIndex(enabled ? initialIndexRef.current : -1);
6893
+ }, [enabled]);
6894
+ const getNextIndex = React.useCallback((current, direction) => {
6895
+ if (itemCount === 0) {
6896
+ return -1;
6897
+ }
6898
+ let next = current + direction;
6899
+ for (let i = 0; i < itemCount; i++) {
6900
+ if (next < 0) {
6901
+ next = loop ? itemCount - 1 : 0;
6902
+ }
6903
+ if (next >= itemCount) {
6904
+ next = loop ? 0 : itemCount - 1;
6905
+ }
6906
+ if (!isDisabled?.(next)) {
6907
+ return next;
6908
+ }
6909
+ next += direction;
6910
+ }
6911
+ return current;
6912
+ }, [itemCount, loop, isDisabled]);
6913
+ useKeyPress({
6914
+ ArrowDown: (e) => {
6915
+ e.preventDefault();
6916
+ setFocusedIndex((prev) => getNextIndex(prev, 1));
6917
+ },
6918
+ ArrowUp: (e) => {
6919
+ e.preventDefault();
6920
+ setFocusedIndex((prev) => getNextIndex(prev, -1));
6921
+ },
6922
+ Home: (e) => {
6923
+ e.preventDefault();
6924
+ setFocusedIndex(getNextIndex(-1, 1));
6925
+ },
6926
+ End: (e) => {
6927
+ e.preventDefault();
6928
+ setFocusedIndex(getNextIndex(itemCount, -1));
6929
+ },
6930
+ Enter: () => {
6931
+ if (focusedIndex >= 0) {
6932
+ onSelect(focusedIndex);
6933
+ }
6934
+ },
6935
+ }, { enabled });
6936
+ return { focusedIndex, setFocusedIndex };
6937
+ };
6938
+
6769
6939
  const lightPalette = {
6770
6940
  // Surface
6771
6941
  surfaceBackground: '#f8fafc',
@@ -7226,5 +7396,17 @@ exports.ToggleIconButton = ToggleIconButton;
7226
7396
  exports.Tooltip = Tooltip;
7227
7397
  exports.darkTheme = darkTheme;
7228
7398
  exports.lightTheme = lightTheme;
7399
+ exports.useBodyScrollLock = useBodyScrollLock;
7400
+ exports.useBreakpointMax = useBreakpointMax;
7401
+ exports.useBreakpointMin = useBreakpointMin;
7402
+ exports.useControllableState = useControllableState;
7229
7403
  exports.useDrawerContext = useDrawerContext;
7404
+ exports.useFocusTrap = useFocusTrap;
7405
+ exports.useKeyPress = useKeyPress;
7406
+ exports.useListKeyNav = useListKeyNav;
7407
+ exports.useMediaQuery = useMediaQuery;
7408
+ exports.useMenuPosition = useMenuPosition;
7409
+ exports.useMergedRefs = useMergedRefs;
7410
+ exports.useTooltipPosition = useTooltipPosition;
7411
+ exports.useTransitionRender = useTransitionRender;
7230
7412
  //# sourceMappingURL=index.js.map