@aurora-ds/components 1.1.6 → 1.2.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 +710 -224
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +708 -226
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +386 -34
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
|
|
2
|
-
import { keyframes,
|
|
2
|
+
import { keyframes, createStyles, useTheme, cx, createVariants, createTheme } from '@aurora-ds/theme';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { createElement, Fragment, useRef, useState, useCallback, useEffect, useId, isValidElement, cloneElement, useLayoutEffect, useMemo, createContext, useContext } from 'react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
@@ -161,24 +161,32 @@ const skeletonShimmerAnimation = keyframes({
|
|
|
161
161
|
'100%': { backgroundPosition: '0% 50%' },
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
164
|
+
/** Default duration in milliseconds for mount/unmount transition animations. */
|
|
165
|
+
const DEFAULT_TRANSITION_DURATION_MS = 250;
|
|
166
|
+
const DEFAULT_BUTTON_HEIGHT = 40;
|
|
167
|
+
const DEFAULT_DRAWER_ITEM_SIZE = 40;
|
|
168
|
+
/** Drawer widths (px) — single source of truth for both the Drawer and its items. */
|
|
169
|
+
const EXPANDED_DRAWER_WIDTH = 240;
|
|
170
|
+
const COLLAPSED_DRAWER_WIDTH = 58;
|
|
171
|
+
/**
|
|
172
|
+
* Horizontal padding (px) applied by `Drawer.Body` on EACH side of its items
|
|
173
|
+
* (Box `px="sm"` → theme.spacing.sm = 0.5rem = 8px).
|
|
174
|
+
*/
|
|
175
|
+
const DRAWER_BODY_HORIZONTAL_PADDING = 8;
|
|
176
|
+
/**
|
|
177
|
+
* DrawerItem widths (px), always derived from the drawer width minus the body
|
|
178
|
+
* horizontal padding. Using explicit widths (instead of `width: 100%`) lets the
|
|
179
|
+
* item animate its own width in sync with the drawer for a smooth transition.
|
|
180
|
+
*/
|
|
181
|
+
const EXPANDED_DRAWER_ITEM_WIDTH = EXPANDED_DRAWER_WIDTH - DRAWER_BODY_HORIZONTAL_PADDING * 2; // 264
|
|
182
|
+
const COLLAPSED_DRAWER_ITEM_WIDTH = COLLAPSED_DRAWER_WIDTH - DRAWER_BODY_HORIZONTAL_PADDING * 2; // 44
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Returns the complete root styles for an action button (base + color/variant),
|
|
186
|
+
* all in one object so CSS transitions work correctly.
|
|
187
|
+
* Size-specific styles (height, padding, fontSize) are added by the caller.
|
|
188
|
+
*/
|
|
189
|
+
const buildActionButtonRootStyle = (theme, variant, color) => {
|
|
182
190
|
const c = theme.colors;
|
|
183
191
|
const intents = {
|
|
184
192
|
primary: {
|
|
@@ -217,57 +225,60 @@ const buildActionButtonCompoundVariants = (theme) => {
|
|
|
217
225
|
fg: c.errorMain, fgHover: c.errorHover, border: c.errorMain,
|
|
218
226
|
},
|
|
219
227
|
};
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
};
|
|
228
|
+
const intent = intents[color];
|
|
229
|
+
const colorVariantStyles = variant === 'contained'
|
|
230
|
+
? {
|
|
231
|
+
backgroundColor: intent.main,
|
|
232
|
+
borderColor: intent.main,
|
|
233
|
+
color: intent.on,
|
|
234
|
+
boxShadow: theme.shadows.xs,
|
|
235
|
+
':hover:not(:disabled)': { backgroundColor: intent.hover, borderColor: intent.hover, boxShadow: theme.shadows.sm },
|
|
236
|
+
':active:not(:disabled)': { backgroundColor: intent.active, borderColor: intent.active, boxShadow: theme.shadows.none },
|
|
230
237
|
}
|
|
231
|
-
|
|
232
|
-
|
|
238
|
+
: variant === 'outlined'
|
|
239
|
+
? {
|
|
233
240
|
backgroundColor: 'transparent',
|
|
234
241
|
borderColor: intent.border,
|
|
235
242
|
color: intent.fg,
|
|
236
243
|
':hover:not(:disabled)': { backgroundColor: intent.subtleHover, color: intent.fgHover },
|
|
237
244
|
':active:not(:disabled)': { backgroundColor: intent.subtleActive, borderColor: intent.active, color: intent.active },
|
|
245
|
+
}
|
|
246
|
+
: {
|
|
247
|
+
backgroundColor: 'transparent',
|
|
248
|
+
borderColor: 'transparent',
|
|
249
|
+
color: intent.fg,
|
|
250
|
+
':hover:not(:disabled)': { backgroundColor: intent.subtleHover, color: intent.fgHover },
|
|
251
|
+
':active:not(:disabled)': { backgroundColor: intent.subtleActive, color: intent.active },
|
|
238
252
|
};
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
253
|
+
return {
|
|
254
|
+
position: 'relative',
|
|
255
|
+
display: 'inline-flex',
|
|
256
|
+
alignItems: 'center',
|
|
257
|
+
justifyContent: 'center',
|
|
258
|
+
boxSizing: 'border-box',
|
|
259
|
+
border: '1px solid transparent',
|
|
260
|
+
borderRadius: theme.radius.md,
|
|
261
|
+
fontFamily: 'inherit',
|
|
262
|
+
userSelect: 'none',
|
|
263
|
+
height: DEFAULT_BUTTON_HEIGHT,
|
|
264
|
+
cursor: 'pointer',
|
|
265
|
+
outline: 'none',
|
|
266
|
+
transition: `background-color ${theme.transition.normal}, border-color ${theme.transition.normal}, color ${theme.transition.normal}, box-shadow ${theme.transition.normal}`,
|
|
267
|
+
...colorVariantStyles,
|
|
268
|
+
':focus-visible': { boxShadow: theme.shadows.focus },
|
|
269
|
+
':disabled': { cursor: 'not-allowed', opacity: theme.opacity.high, boxShadow: 'none' },
|
|
247
270
|
};
|
|
248
|
-
return Object.keys(intents).flatMap((color) => APPEARANCES.map((appearance) => ({
|
|
249
|
-
color,
|
|
250
|
-
variant: appearance,
|
|
251
|
-
styles: appearanceStyles(intents[color], appearance),
|
|
252
|
-
})));
|
|
253
271
|
};
|
|
254
272
|
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
size
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
variant: { contained: {}, outlined: {}, text: {} },
|
|
265
|
-
color: { primary: {}, secondary: {}, neutral: {}, info: {}, success: {}, warning: {}, error: {} },
|
|
266
|
-
},
|
|
267
|
-
defaultVariants: { size: 'md', variant: 'contained', color: 'primary' },
|
|
268
|
-
compoundVariants: buildActionButtonCompoundVariants(theme),
|
|
269
|
-
}), { id: 'button' });
|
|
270
|
-
const BUTTON_STYLES = createStyles({
|
|
273
|
+
const BUTTON_STYLES = createStyles((theme) => ({
|
|
274
|
+
root: ({ variant, color, size }) => ({
|
|
275
|
+
...buildActionButtonRootStyle(theme, variant, color),
|
|
276
|
+
...(size === 'sm'
|
|
277
|
+
? { height: '2rem', padding: `0 ${theme.spacing.sm}`, fontSize: theme.fontSize.xs }
|
|
278
|
+
: size === 'lg'
|
|
279
|
+
? { height: '3rem', padding: `0 ${theme.spacing.lg}`, fontSize: theme.fontSize.md }
|
|
280
|
+
: { height: '2.5rem', padding: `0 ${theme.spacing.md}`, fontSize: theme.fontSize.sm }),
|
|
281
|
+
}),
|
|
271
282
|
/** Inner wrapper holding icons + label, centered by the button. */
|
|
272
283
|
content: {
|
|
273
284
|
display: 'inline-flex',
|
|
@@ -290,7 +301,14 @@ const BUTTON_STYLES = createStyles({
|
|
|
290
301
|
animation: `${spinAnimation} 0.75s linear infinite`,
|
|
291
302
|
'@media (prefers-reduced-motion: reduce)': { animation: 'none' },
|
|
292
303
|
},
|
|
293
|
-
}, { id: 'button
|
|
304
|
+
}), { id: 'button' });
|
|
305
|
+
// Pre-generate CSS for all variant/color/size combinations at module load.
|
|
306
|
+
// This ensures the CSS is already in the stylesheet before the first user interaction,
|
|
307
|
+
// preventing the "first click is instant" issue caused by lazy CSS injection.
|
|
308
|
+
const BUTTON_VARIANT_VALUES = ['contained', 'outlined', 'text'];
|
|
309
|
+
const BUTTON_COLOR_VALUES = ['primary', 'secondary', 'neutral', 'info', 'success', 'warning', 'error'];
|
|
310
|
+
const BUTTON_SIZE_VALUES = ['sm', 'md', 'lg'];
|
|
311
|
+
BUTTON_VARIANT_VALUES.forEach(variant => BUTTON_COLOR_VALUES.forEach(color => BUTTON_SIZE_VALUES.forEach(size => BUTTON_STYLES.root({ variant, color, size }))));
|
|
294
312
|
|
|
295
313
|
const ICON_STYLES = createStyles((theme) => ({
|
|
296
314
|
root: ({ size, strokeColor, fill, backgroundColor, padding, borderRadius }) => ({
|
|
@@ -556,35 +574,29 @@ const ICON_SIZE$2 = {
|
|
|
556
574
|
* @example <Button label='Delete' variant='outlined' color='error' startIcon={IconRegistry.CloseIcon} />
|
|
557
575
|
* @example <Button label='Submitting…' color='success' isLoading width='100%' />
|
|
558
576
|
*/
|
|
559
|
-
const Button = ({ ref, variant = 'contained', color = 'primary', size = 'md', width, flexGrow, flexShrink, isLoading = false, startIcon: StartIcon, endIcon: EndIcon, label, className, type = 'button', disabled, style, ...rest }) => {
|
|
577
|
+
const Button = ({ ref, variant = 'contained', color = 'primary', size = 'md', width, flexGrow, flexShrink, isLoading = false, startIcon: StartIcon, endIcon: EndIcon, label, children, className, type = 'button', disabled, style, ...rest }) => {
|
|
560
578
|
const isDisabled = disabled || isLoading;
|
|
561
579
|
const iconSize = ICON_SIZE$2[size];
|
|
562
|
-
const rootClassName =
|
|
580
|
+
const rootClassName = cx(BUTTON_STYLES.root({ variant, color, size }), className);
|
|
563
581
|
const mergedStyle = {
|
|
564
582
|
...style,
|
|
565
583
|
...(width !== undefined ? { width } : {}),
|
|
566
584
|
...(flexGrow !== undefined ? { flexGrow } : {}),
|
|
567
585
|
...(flexShrink !== undefined ? { flexShrink } : {}),
|
|
568
586
|
};
|
|
569
|
-
return (jsxs("button", { ref: ref, type: type, className: rootClassName, disabled: isDisabled, "aria-busy": isLoading || undefined, style: mergedStyle, ...rest, children: [isLoading && (jsx("span", { className: BUTTON_STYLES.spinnerWrap, children: jsx(Icon, { icon: SpinnerIcon, size: iconSize, className: BUTTON_STYLES.spinnerIcon }) })), jsxs("span", { className: cx(BUTTON_STYLES.content, isLoading && BUTTON_STYLES.contentHidden), children: [StartIcon && (jsx(Icon, { icon: StartIcon, size: iconSize })), label && (jsx(Text, { variant: 'span', fontSize: LABEL_FONT_SIZE$1[size], fontWeight: 'medium', lineHeight: 'none', children: label })), EndIcon && (jsx(Icon, { icon: EndIcon, size: iconSize }))] })] }));
|
|
587
|
+
return (jsxs("button", { ref: ref, type: type, className: rootClassName, disabled: isDisabled, "aria-busy": isLoading || undefined, style: mergedStyle, ...rest, children: [isLoading && (jsx("span", { className: BUTTON_STYLES.spinnerWrap, children: jsx(Icon, { icon: SpinnerIcon, size: iconSize, className: BUTTON_STYLES.spinnerIcon }) })), jsxs("span", { className: cx(BUTTON_STYLES.content, isLoading && BUTTON_STYLES.contentHidden), children: [StartIcon && (jsx(Icon, { icon: StartIcon, size: iconSize })), (label !== undefined || children !== undefined) && (jsx(Text, { variant: 'span', fontSize: LABEL_FONT_SIZE$1[size], fontWeight: 'medium', lineHeight: 'none', children: label ?? children })), EndIcon && (jsx(Icon, { icon: EndIcon, size: iconSize }))] })] }));
|
|
570
588
|
};
|
|
571
589
|
Button.displayName = 'Button';
|
|
572
590
|
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
size
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
color: { primary: {}, secondary: {}, neutral: {}, info: {}, success: {}, warning: {}, error: {} },
|
|
583
|
-
},
|
|
584
|
-
defaultVariants: { size: 'md', variant: 'contained', color: 'primary' },
|
|
585
|
-
compoundVariants: buildActionButtonCompoundVariants(theme),
|
|
586
|
-
}), { id: 'icon-button' });
|
|
587
|
-
const ICON_BUTTON_STYLES = createStyles({
|
|
591
|
+
const ICON_BUTTON_STYLES = createStyles((theme) => ({
|
|
592
|
+
root: ({ variant, color, size }) => ({
|
|
593
|
+
...buildActionButtonRootStyle(theme, variant, color),
|
|
594
|
+
...(size === 'sm'
|
|
595
|
+
? { width: '2rem', height: '2rem', padding: '0' }
|
|
596
|
+
: size === 'lg'
|
|
597
|
+
? { width: '3rem', height: '3rem', padding: '0' }
|
|
598
|
+
: { width: '2.5rem', height: '2.5rem', padding: '0' }),
|
|
599
|
+
}),
|
|
588
600
|
/** Spinning animation applied to the SpinnerIcon. */
|
|
589
601
|
spinnerIcon: {
|
|
590
602
|
animation: `${spinAnimation} 0.75s linear infinite`,
|
|
@@ -600,7 +612,11 @@ const ICON_BUTTON_STYLES = createStyles({
|
|
|
600
612
|
alignItems: 'center',
|
|
601
613
|
justifyContent: 'center',
|
|
602
614
|
},
|
|
603
|
-
}, { id: 'icon-button
|
|
615
|
+
}), { id: 'icon-button' });
|
|
616
|
+
const ICON_BUTTON_VARIANT_VALUES = ['contained', 'outlined', 'text'];
|
|
617
|
+
const ICON_BUTTON_COLOR_VALUES = ['primary', 'secondary', 'neutral', 'info', 'success', 'warning', 'error'];
|
|
618
|
+
const ICON_BUTTON_SIZE_VALUES = ['sm', 'md', 'lg'];
|
|
619
|
+
ICON_BUTTON_VARIANT_VALUES.forEach(variant => ICON_BUTTON_COLOR_VALUES.forEach(color => ICON_BUTTON_SIZE_VALUES.forEach(size => ICON_BUTTON_STYLES.root({ variant, color, size }))));
|
|
604
620
|
|
|
605
621
|
/** Maps the icon-button size to an Icon size token. */
|
|
606
622
|
const ICON_SIZE$1 = {
|
|
@@ -619,7 +635,7 @@ const ICON_SIZE$1 = {
|
|
|
619
635
|
const IconButton = ({ ref, icon: IconComponent, ariaLabel, variant = 'contained', color = 'primary', size = 'md', isLoading = false, className, type = 'button', disabled, ...rest }) => {
|
|
620
636
|
const isDisabled = disabled || isLoading;
|
|
621
637
|
const iconSize = ICON_SIZE$1[size];
|
|
622
|
-
const rootClassName =
|
|
638
|
+
const rootClassName = cx(ICON_BUTTON_STYLES.root({ variant, color, size }), className);
|
|
623
639
|
return (jsxs("button", { ref: ref, type: type, className: rootClassName, disabled: isDisabled, "aria-label": ariaLabel, "aria-busy": isLoading || undefined, ...rest, children: [isLoading && (jsx("span", { className: ICON_BUTTON_STYLES.spinnerWrap, children: jsx(Icon, { icon: SpinnerIcon, size: iconSize, className: ICON_BUTTON_STYLES.spinnerIcon }) })), jsx(Icon, { icon: IconComponent, size: iconSize, className: cx(isLoading && ICON_BUTTON_STYLES.iconHidden) })] }));
|
|
624
640
|
};
|
|
625
641
|
IconButton.displayName = 'IconButton';
|
|
@@ -667,12 +683,20 @@ const LINK_STYLES = createStyles((theme) => ({
|
|
|
667
683
|
/**
|
|
668
684
|
* Theme-aware anchor element with optional icons and underline control.
|
|
669
685
|
*
|
|
686
|
+
* Supports SPA navigation (e.g. React Router) via `onClick` without `href`.
|
|
687
|
+
* In that case the component stays accessible: it gets `role="link"`,
|
|
688
|
+
* `tabIndex={0}` and keyboard Enter support automatically.
|
|
689
|
+
*
|
|
670
690
|
* @example <Link href='/about'>About</Link>
|
|
671
691
|
* @example <Link href='https://example.com' external>External site</Link>
|
|
672
692
|
* @example <Link href='/profile' underline='always' startIcon={UserIcon}>Profile</Link>
|
|
673
693
|
* @example <Link href='/terms' underline='none'>Terms</Link>
|
|
694
|
+
* @example <Link onClick={() => navigate('/about')}>About (SPA)</Link>
|
|
674
695
|
*/
|
|
675
|
-
const Link = ({ ref, underline = 'hover', external = false, disabled = false, startIcon: StartIcon, endIcon: EndIcon, children, className, onClick, onKeyDown, ...rest }) => {
|
|
696
|
+
const Link = ({ ref, underline = 'hover', external = false, disabled = false, startIcon: StartIcon, endIcon: EndIcon, children, className, href, onClick, onKeyDown, ...rest }) => {
|
|
697
|
+
// An <a> without href has no implicit ARIA role and is not focusable.
|
|
698
|
+
// When used for SPA navigation (onClick only), we restore both behaviours.
|
|
699
|
+
const hasHref = !!href;
|
|
676
700
|
const handleClick = (e) => {
|
|
677
701
|
if (disabled) {
|
|
678
702
|
e.preventDefault();
|
|
@@ -680,14 +704,22 @@ const Link = ({ ref, underline = 'hover', external = false, disabled = false, st
|
|
|
680
704
|
}
|
|
681
705
|
onClick?.(e);
|
|
682
706
|
};
|
|
683
|
-
// Prevents Enter navigation when disabled; satisfies jsx-a11y/click-events-have-key-events.
|
|
684
707
|
const handleKeyDown = (e) => {
|
|
685
708
|
if (disabled && e.key === 'Enter') {
|
|
686
709
|
e.preventDefault();
|
|
687
710
|
}
|
|
711
|
+
// Without href, the browser does not fire a click on Enter natively.
|
|
712
|
+
if (!hasHref && !disabled && e.key === 'Enter') {
|
|
713
|
+
e.currentTarget.click();
|
|
714
|
+
}
|
|
688
715
|
onKeyDown?.(e);
|
|
689
716
|
};
|
|
690
|
-
return (jsxs("a", { ref: ref, className: cx(LINK_STYLES.root({ underline }), className), "aria-disabled": disabled || undefined,
|
|
717
|
+
return (jsxs("a", { ref: ref, href: href, className: cx(LINK_STYLES.root({ underline }), className), "aria-disabled": disabled || undefined,
|
|
718
|
+
// Without href: must be explicitly put in the tab order.
|
|
719
|
+
// With href: the browser handles focusability natively (no tabIndex needed).
|
|
720
|
+
tabIndex: disabled ? -1 : (!hasHref ? 0 : undefined),
|
|
721
|
+
// Without href: <a> has no implicit ARIA role — add role="link" explicitly.
|
|
722
|
+
role: !hasHref ? 'link' : undefined, target: external ? '_blank' : undefined, rel: external ? 'noopener noreferrer' : undefined, onClick: handleClick, onKeyDown: handleKeyDown, ...rest, children: [StartIcon && (jsx("span", { className: LINK_STYLES.icon, "aria-hidden": true, children: jsx(StartIcon, { width: '1em', height: '1em' }) })), children, EndIcon && (jsx("span", { className: LINK_STYLES.icon, "aria-hidden": true, children: jsx(EndIcon, { width: '1em', height: '1em' }) }))] }));
|
|
691
723
|
};
|
|
692
724
|
Link.displayName = 'Link';
|
|
693
725
|
|
|
@@ -835,7 +867,7 @@ const BADGE_VARIANTS = createVariants((theme) => ({
|
|
|
835
867
|
size: {
|
|
836
868
|
sm: {
|
|
837
869
|
height: '1.25rem',
|
|
838
|
-
padding: `0.125rem ${theme.spacing
|
|
870
|
+
padding: `0.125rem ${theme.spacing.xsPlus}`,
|
|
839
871
|
fontSize: theme.fontSize['2xs'],
|
|
840
872
|
},
|
|
841
873
|
md: {
|
|
@@ -1565,6 +1597,18 @@ Box.displayName = 'Box';
|
|
|
1565
1597
|
const Stack = ({ flexDirection = 'row', display = 'flex', gap = 'sm', ...rest }) => (jsx(Box, { display: display, flexDirection: flexDirection, gap: gap, ...rest }));
|
|
1566
1598
|
Stack.displayName = 'Stack';
|
|
1567
1599
|
|
|
1600
|
+
const HELPER_COLOR_MAP = {
|
|
1601
|
+
default: 'textSecondary',
|
|
1602
|
+
error: 'errorHover',
|
|
1603
|
+
success: 'successHover',
|
|
1604
|
+
warning: 'warningHover',
|
|
1605
|
+
};
|
|
1606
|
+
const FormHelperText = ({ id, status = 'default', ariaLive, className, children, }) => {
|
|
1607
|
+
const resolvedAriaLive = ariaLive ?? (status === 'error' ? 'assertive' : 'polite');
|
|
1608
|
+
return (jsx(Text, { id: id, variant: 'span', fontSize: 'xs', color: HELPER_COLOR_MAP[status], "aria-live": resolvedAriaLive, className: className, children: children }));
|
|
1609
|
+
};
|
|
1610
|
+
FormHelperText.displayName = 'FormHelperText';
|
|
1611
|
+
|
|
1568
1612
|
const TEXTFIELD_WRAPPER_VARIANTS = createVariants((theme) => {
|
|
1569
1613
|
const c = theme.colors;
|
|
1570
1614
|
return {
|
|
@@ -1687,18 +1731,11 @@ const ICON_BUTTON_SIZE_MAP = {
|
|
|
1687
1731
|
md: 'sm',
|
|
1688
1732
|
lg: 'md',
|
|
1689
1733
|
};
|
|
1690
|
-
/** Maps status to a theme color token used for the helper text. */
|
|
1691
|
-
const HELPER_COLOR_MAP$1 = {
|
|
1692
|
-
default: 'textSecondary',
|
|
1693
|
-
error: 'errorHover',
|
|
1694
|
-
success: 'successHover',
|
|
1695
|
-
warning: 'warningHover',
|
|
1696
|
-
};
|
|
1697
1734
|
/**
|
|
1698
1735
|
* Business logic for the TextField component: id resolution, ref merging,
|
|
1699
1736
|
* password visibility toggling and size/status derived tokens.
|
|
1700
1737
|
*/
|
|
1701
|
-
const useTextField = ({ id, ref, type, size,
|
|
1738
|
+
const useTextField = ({ id, ref, type, size, endAction, }) => {
|
|
1702
1739
|
const generatedId = useId();
|
|
1703
1740
|
const fieldId = id ?? generatedId;
|
|
1704
1741
|
const helperId = `${fieldId}-helper`;
|
|
@@ -1720,7 +1757,6 @@ const useTextField = ({ id, ref, type, size, status, endAction, }) => {
|
|
|
1720
1757
|
resolvedType,
|
|
1721
1758
|
iconSize: ICON_SIZE_MAP$1[size],
|
|
1722
1759
|
iconButtonSize: ICON_BUTTON_SIZE_MAP[size],
|
|
1723
|
-
helperColor: HELPER_COLOR_MAP$1[status],
|
|
1724
1760
|
hasEndSection: endAction !== undefined || isPassword,
|
|
1725
1761
|
focusInput,
|
|
1726
1762
|
};
|
|
@@ -1738,8 +1774,8 @@ const useTextField = ({ id, ref, type, size, status, endAction, }) => {
|
|
|
1738
1774
|
* @example <TextField label="Search" startIcon={SearchIcon} endAction={<ClearButton />} />
|
|
1739
1775
|
*/
|
|
1740
1776
|
const TextField = ({ ref, label, helperText, size = 'md', status = 'default', startIcon: StartIcon, endAction, type, id, disabled, required, ...rest }) => {
|
|
1741
|
-
const { fieldId, helperId, mergedRef, isPassword, showPassword, togglePassword, resolvedType, iconSize, iconButtonSize,
|
|
1742
|
-
return (jsxs(Stack, { flexDirection: 'column', gap: 'xs', children: [label !== undefined && (jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, children: [label, required && (jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsxs("div", { className: TEXTFIELD_WRAPPER_VARIANTS({ size, status }), "data-disabled": disabled || undefined, children: [StartIcon && (jsx("span", { className: TEXTFIELD_STYLES.startIconWrap, onClick: focusInput, children: jsx(Icon, { icon: StartIcon, size: iconSize, strokeColor: 'textSecondary' }) })), 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, className: TEXTFIELD_STYLES.input, ...rest }), hasEndSection && (jsxs("span", { className: TEXTFIELD_STYLES.endActionWrap, children: [endAction, isPassword && (jsx(IconButton, { icon: showPassword ? EyeSlashIcon : EyeIcon, ariaLabel: showPassword ? 'Hide password' : 'Show password', variant: 'text', color: 'neutral', size: iconButtonSize, type: 'button',
|
|
1777
|
+
const { fieldId, helperId, mergedRef, isPassword, showPassword, togglePassword, resolvedType, iconSize, iconButtonSize, hasEndSection, focusInput, } = useTextField({ id, ref, type, size, endAction });
|
|
1778
|
+
return (jsxs(Stack, { flexDirection: 'column', gap: 'xs', children: [label !== undefined && (jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, children: [label, required && (jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsxs("div", { className: TEXTFIELD_WRAPPER_VARIANTS({ size, status }), "data-disabled": disabled || undefined, children: [StartIcon && (jsx("span", { className: TEXTFIELD_STYLES.startIconWrap, onClick: focusInput, "aria-hidden": true, children: jsx(Icon, { icon: StartIcon, size: iconSize, strokeColor: 'textSecondary' }) })), 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 && (jsxs("span", { className: TEXTFIELD_STYLES.endActionWrap, children: [endAction, isPassword && (jsx(IconButton, { icon: showPassword ? EyeSlashIcon : EyeIcon, ariaLabel: showPassword ? 'Hide password' : 'Show password', variant: 'text', color: 'neutral', size: iconButtonSize, type: 'button', onClick: togglePassword }))] }))] }), helperText !== undefined && (jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
|
|
1743
1779
|
};
|
|
1744
1780
|
TextField.displayName = 'TextField';
|
|
1745
1781
|
|
|
@@ -1874,7 +1910,9 @@ const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, }) => {
|
|
|
1874
1910
|
*/
|
|
1875
1911
|
const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
|
|
1876
1912
|
const panelRef = useRef(null);
|
|
1913
|
+
const baseId = useId();
|
|
1877
1914
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
1915
|
+
const [activeDescendant, setActiveDescendant] = useState(undefined);
|
|
1878
1916
|
const { style } = useMenuPosition({ anchorEl, open, menuRef: panelRef, minWidth });
|
|
1879
1917
|
/** Returns all non-disabled option elements inside the panel. */
|
|
1880
1918
|
const getOptions = useCallback(() => {
|
|
@@ -1897,22 +1935,31 @@ const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
|
|
|
1897
1935
|
});
|
|
1898
1936
|
return () => cancelAnimationFrame(raf);
|
|
1899
1937
|
}, [open, getOptions]);
|
|
1900
|
-
// Keep data-focused
|
|
1938
|
+
// Keep data-focused (visual highlight) and aria-activedescendant (screen reader
|
|
1939
|
+
// announcement) in sync with focusedIndex. Each option is assigned a stable id
|
|
1940
|
+
// so the listbox can reference the active one via aria-activedescendant.
|
|
1901
1941
|
useEffect(() => {
|
|
1902
1942
|
if (!open) {
|
|
1943
|
+
setActiveDescendant(undefined);
|
|
1903
1944
|
return;
|
|
1904
1945
|
}
|
|
1905
1946
|
const options = getOptions();
|
|
1947
|
+
let activeId;
|
|
1906
1948
|
options.forEach((el, idx) => {
|
|
1949
|
+
if (!el.id) {
|
|
1950
|
+
el.id = `${baseId}-option-${idx}`;
|
|
1951
|
+
}
|
|
1907
1952
|
if (idx === focusedIndex) {
|
|
1908
1953
|
el.setAttribute('data-focused', 'true');
|
|
1909
1954
|
el.scrollIntoView({ block: 'nearest' });
|
|
1955
|
+
activeId = el.id;
|
|
1910
1956
|
}
|
|
1911
1957
|
else {
|
|
1912
1958
|
el.removeAttribute('data-focused');
|
|
1913
1959
|
}
|
|
1914
1960
|
});
|
|
1915
|
-
|
|
1961
|
+
setActiveDescendant(activeId);
|
|
1962
|
+
}, [focusedIndex, open, getOptions, baseId]);
|
|
1916
1963
|
useKeyPress({
|
|
1917
1964
|
Escape: onClose,
|
|
1918
1965
|
ArrowDown: (e) => {
|
|
@@ -1950,7 +1997,7 @@ const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
|
|
|
1950
1997
|
}
|
|
1951
1998
|
},
|
|
1952
1999
|
}, { enabled: open });
|
|
1953
|
-
return { panelRef, style };
|
|
2000
|
+
return { panelRef, style, activeDescendant };
|
|
1954
2001
|
};
|
|
1955
2002
|
|
|
1956
2003
|
const MENU_GROUP_STYLES = createStyles((theme) => {
|
|
@@ -1991,7 +2038,8 @@ const MENU_GROUP_STYLES = createStyles((theme) => {
|
|
|
1991
2038
|
}, { id: 'menu-group' });
|
|
1992
2039
|
|
|
1993
2040
|
const MenuGroup = ({ label, divider, children, }) => {
|
|
1994
|
-
|
|
2041
|
+
const labelId = useId();
|
|
2042
|
+
return (jsxs("div", { className: MENU_GROUP_STYLES.root, children: [divider && (jsx("div", { className: MENU_GROUP_STYLES.divider, role: 'separator', "aria-hidden": true })), label !== undefined && (jsx("span", { id: labelId, className: MENU_GROUP_STYLES.label, "aria-hidden": true, children: label })), jsx("ul", { className: MENU_GROUP_STYLES.list, role: 'group', "aria-labelledby": label !== undefined ? labelId : undefined, children: children })] }));
|
|
1995
2043
|
};
|
|
1996
2044
|
MenuGroup.displayName = 'MenuGroup';
|
|
1997
2045
|
|
|
@@ -2042,12 +2090,12 @@ const MenuItem = ({ ref, label, icon, selected, focused, disabled, onClick, ...r
|
|
|
2042
2090
|
};
|
|
2043
2091
|
MenuItem.displayName = 'MenuItem';
|
|
2044
2092
|
|
|
2045
|
-
const MenuBase = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', id, children, }) => {
|
|
2046
|
-
const { panelRef, style } = useMenu({ open, onClose, anchorEl, minWidth });
|
|
2093
|
+
const MenuBase = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, children, }) => {
|
|
2094
|
+
const { panelRef, style, activeDescendant } = useMenu({ open, onClose, anchorEl, minWidth });
|
|
2047
2095
|
if (!open) {
|
|
2048
2096
|
return null;
|
|
2049
2097
|
}
|
|
2050
|
-
return createPortal(jsxs(Fragment$1, { children: [jsx("div", { className: MENU_STYLES.backdrop, onClick: onClose, "aria-hidden": true }), jsx("div", { ref: panelRef, id: id, role: 'listbox', tabIndex: -1, className: MENU_STYLES.panel, style: { ...style, maxHeight, outline: 'none' }, children: children })] }), document.body);
|
|
2098
|
+
return createPortal(jsxs(Fragment$1, { children: [jsx("div", { className: MENU_STYLES.backdrop, onClick: onClose, "aria-hidden": true }), jsx("div", { ref: panelRef, id: id, role: 'listbox', tabIndex: -1, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-activedescendant": activeDescendant, className: MENU_STYLES.panel, style: { ...style, maxHeight, outline: 'none' }, children: children })] }), document.body);
|
|
2051
2099
|
};
|
|
2052
2100
|
MenuBase.displayName = 'Menu';
|
|
2053
2101
|
const Menu = MenuBase;
|
|
@@ -2169,28 +2217,23 @@ const ICON_SIZE_MAP = {
|
|
|
2169
2217
|
md: 'md',
|
|
2170
2218
|
lg: 'lg',
|
|
2171
2219
|
};
|
|
2172
|
-
const SelectTrigger = ({ ref, size = 'md', status = 'default', open, hasValue, startIcon, disabled, children, ...rest }) => {
|
|
2220
|
+
const SelectTrigger = ({ ref, size = 'md', status = 'default', open, hasValue, startIcon, disabled, children, 'aria-expanded': ariaExpanded, 'aria-controls': ariaControls, ...rest }) => {
|
|
2173
2221
|
const iconSize = ICON_SIZE_MAP[size];
|
|
2174
|
-
return (jsxs("button", { type: 'button', ref: ref, className: SELECT_TRIGGER_VARIANTS({ size, status }), "data-open": open || undefined, "data-disabled": disabled || undefined, disabled: disabled, ...rest, children: [startIcon !== undefined && (jsx(Icon, { icon: startIcon, size: iconSize, strokeColor: 'textSecondary' })), jsx("span", { className: hasValue ? SELECT_TRIGGER_STYLES.value : SELECT_TRIGGER_STYLES.placeholder, children: children }), jsx(Icon, { icon: ChevronDownIcon, size: iconSize, className: cx(SELECT_TRIGGER_STYLES.chevron, open ? SELECT_TRIGGER_STYLES.chevronOpen : undefined), strokeColor: 'textSecondary' })] }));
|
|
2222
|
+
return (jsxs("button", { type: 'button', role: 'combobox', ref: ref, className: SELECT_TRIGGER_VARIANTS({ size, status }), "data-open": open || undefined, "data-disabled": disabled || undefined, disabled: disabled, "aria-expanded": ariaExpanded, "aria-controls": ariaControls, ...rest, children: [startIcon !== undefined && (jsx(Icon, { icon: startIcon, size: iconSize, strokeColor: 'textSecondary' })), jsx("span", { className: hasValue ? SELECT_TRIGGER_STYLES.value : SELECT_TRIGGER_STYLES.placeholder, children: children }), jsx(Icon, { icon: ChevronDownIcon, size: iconSize, className: cx(SELECT_TRIGGER_STYLES.chevron, open ? SELECT_TRIGGER_STYLES.chevronOpen : undefined), strokeColor: 'textSecondary' })] }));
|
|
2175
2223
|
};
|
|
2176
2224
|
SelectTrigger.displayName = 'SelectTrigger';
|
|
2177
2225
|
|
|
2178
|
-
/** Maps status to a theme color token used for the helper text. */
|
|
2179
|
-
const HELPER_COLOR_MAP = {
|
|
2180
|
-
default: 'textSecondary',
|
|
2181
|
-
error: 'errorHover',
|
|
2182
|
-
success: 'successHover',
|
|
2183
|
-
warning: 'warningHover',
|
|
2184
|
-
};
|
|
2185
2226
|
/**
|
|
2186
2227
|
* Business logic for the Select component: id resolution, ref merging,
|
|
2187
2228
|
* controlled/uncontrolled value handling, open state, option grouping and
|
|
2188
2229
|
* focus restoration to the trigger when the menu closes.
|
|
2189
2230
|
*/
|
|
2190
|
-
const useSelect = ({ id, ref, value, defaultValue, onChange, options,
|
|
2231
|
+
const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled, }) => {
|
|
2191
2232
|
const generatedId = useId();
|
|
2192
2233
|
const fieldId = id ?? generatedId;
|
|
2234
|
+
const helperId = `${fieldId}-helper`;
|
|
2193
2235
|
const menuId = `${fieldId}-menu`;
|
|
2236
|
+
const labelId = `${fieldId}-label`;
|
|
2194
2237
|
const triggerRef = useRef(null);
|
|
2195
2238
|
const mergedRef = useMergedRefs(ref, triggerRef);
|
|
2196
2239
|
const [open, setOpen] = useState(false);
|
|
@@ -2234,6 +2277,8 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, status, di
|
|
|
2234
2277
|
const close = useCallback(() => setOpen(false), []);
|
|
2235
2278
|
return {
|
|
2236
2279
|
fieldId,
|
|
2280
|
+
labelId,
|
|
2281
|
+
helperId,
|
|
2237
2282
|
menuId,
|
|
2238
2283
|
triggerRef,
|
|
2239
2284
|
mergedRef,
|
|
@@ -2244,19 +2289,187 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, status, di
|
|
|
2244
2289
|
selectedOption,
|
|
2245
2290
|
groupedOptions,
|
|
2246
2291
|
handleSelect,
|
|
2247
|
-
helperColor: HELPER_COLOR_MAP[status],
|
|
2248
2292
|
};
|
|
2249
2293
|
};
|
|
2250
2294
|
|
|
2251
2295
|
const Select = ({ ref, value, defaultValue, onChange, options, label, helperText, placeholder, size = 'md', status = 'default', disabled, required, width, id, }) => {
|
|
2252
|
-
const { fieldId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect,
|
|
2253
|
-
return (jsxs(Stack, { flexDirection: 'column', gap: 'xs', style: { width: width ?? '100%' }, children: [label !== undefined && (jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, children: [label, required && (jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsx(SelectTrigger, { ref: mergedRef, id: fieldId, size: size, status: status, open: open, hasValue: selectedOption !== undefined, disabled: disabled,
|
|
2296
|
+
const { fieldId, labelId, helperId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect, } = useSelect({ id, ref, value, defaultValue, onChange, options, disabled });
|
|
2297
|
+
return (jsxs(Stack, { flexDirection: 'column', gap: 'xs', style: { width: width ?? '100%' }, children: [label !== undefined && (jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, id: labelId, children: [label, required && (jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsx(SelectTrigger, { ref: mergedRef, id: fieldId, size: size, status: status, open: open, hasValue: selectedOption !== undefined, disabled: disabled, "aria-haspopup": 'listbox', "aria-expanded": open, "aria-controls": menuId, "aria-labelledby": label !== undefined ? `${labelId} ${fieldId}` : undefined, "aria-required": required || undefined, "aria-invalid": status === 'error' || undefined, "aria-errormessage": status === 'error' && helperText !== undefined ? helperId : undefined, "aria-describedby": helperText !== undefined ? helperId : undefined, onClick: toggle, children: selectedOption !== undefined ? selectedOption.label : placeholder }), jsx(Menu, { open: open, onClose: close, anchorEl: triggerRef.current, id: menuId, "aria-labelledby": label !== undefined ? labelId : undefined, "aria-label": label === undefined ? placeholder : undefined, children: Array.from(groupedOptions.entries()).map(([groupKey, groupOpts], groupIndex) => {
|
|
2254
2298
|
const items = groupOpts.map((opt) => (jsx(Menu.Item, { label: opt.label, icon: opt.icon, selected: opt.value === currentValue, disabled: opt.disabled, onClick: () => handleSelect(opt.value) }, opt.value)));
|
|
2255
2299
|
return groupKey !== undefined ? (jsx(Menu.Group, { label: groupKey, divider: groupIndex > 0, children: items }, groupKey)) : (jsx(Menu.Group, { divider: groupIndex > 0, children: items }, '__ungrouped'));
|
|
2256
|
-
}) }), helperText !== undefined && (jsx(
|
|
2300
|
+
}) }), helperText !== undefined && (jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
|
|
2257
2301
|
};
|
|
2258
2302
|
Select.displayName = 'Select';
|
|
2259
2303
|
|
|
2304
|
+
const CHECKBOX_ROOT_VARIANTS = createVariants((theme) => ({
|
|
2305
|
+
base: {
|
|
2306
|
+
display: 'inline-flex',
|
|
2307
|
+
alignItems: 'center',
|
|
2308
|
+
gap: theme.spacing.sm,
|
|
2309
|
+
cursor: 'pointer',
|
|
2310
|
+
userSelect: 'none',
|
|
2311
|
+
},
|
|
2312
|
+
variants: {
|
|
2313
|
+
disabled: {
|
|
2314
|
+
true: {
|
|
2315
|
+
cursor: 'not-allowed',
|
|
2316
|
+
opacity: theme.opacity.high,
|
|
2317
|
+
},
|
|
2318
|
+
false: {},
|
|
2319
|
+
},
|
|
2320
|
+
},
|
|
2321
|
+
defaultVariants: {
|
|
2322
|
+
disabled: 'false',
|
|
2323
|
+
},
|
|
2324
|
+
}), { id: 'checkbox-root' });
|
|
2325
|
+
const CHECKBOX_INPUT_VARIANTS = createVariants((theme) => {
|
|
2326
|
+
const c = theme.colors;
|
|
2327
|
+
return {
|
|
2328
|
+
base: {
|
|
2329
|
+
appearance: 'none',
|
|
2330
|
+
position: 'relative',
|
|
2331
|
+
margin: 0,
|
|
2332
|
+
borderWidth: '1px',
|
|
2333
|
+
borderStyle: 'solid',
|
|
2334
|
+
borderColor: c.borderStrong,
|
|
2335
|
+
borderRadius: theme.radius.sm,
|
|
2336
|
+
backgroundColor: c.surfacePaper,
|
|
2337
|
+
transition: `border-color ${theme.transition.fast}, background-color ${theme.transition.fast}`,
|
|
2338
|
+
cursor: 'pointer',
|
|
2339
|
+
'&:focus-visible': {
|
|
2340
|
+
outline: '2px solid transparent',
|
|
2341
|
+
boxShadow: `0 0 0 2px ${c.primarySubtleActive}`,
|
|
2342
|
+
},
|
|
2343
|
+
'&::after': {
|
|
2344
|
+
content: '""',
|
|
2345
|
+
position: 'absolute',
|
|
2346
|
+
left: '50%',
|
|
2347
|
+
top: '50%',
|
|
2348
|
+
transform: 'translate(-50%, -56%) rotate(45deg)',
|
|
2349
|
+
width: '0.25rem',
|
|
2350
|
+
height: '0.5rem',
|
|
2351
|
+
borderRight: `2px solid ${c.textInverse}`,
|
|
2352
|
+
borderBottom: `2px solid ${c.textInverse}`,
|
|
2353
|
+
opacity: 0,
|
|
2354
|
+
},
|
|
2355
|
+
'&:checked::after': {
|
|
2356
|
+
opacity: 1,
|
|
2357
|
+
},
|
|
2358
|
+
'&:indeterminate::after': {
|
|
2359
|
+
width: '0.5rem',
|
|
2360
|
+
height: '0',
|
|
2361
|
+
borderRight: '0',
|
|
2362
|
+
borderBottom: `2px solid ${c.textInverse}`,
|
|
2363
|
+
transform: 'translate(-50%, -50%)',
|
|
2364
|
+
opacity: 1,
|
|
2365
|
+
},
|
|
2366
|
+
},
|
|
2367
|
+
variants: {
|
|
2368
|
+
size: {
|
|
2369
|
+
sm: {
|
|
2370
|
+
width: '1rem',
|
|
2371
|
+
height: '1rem',
|
|
2372
|
+
},
|
|
2373
|
+
md: {
|
|
2374
|
+
width: '1.125rem',
|
|
2375
|
+
height: '1.125rem',
|
|
2376
|
+
},
|
|
2377
|
+
lg: {
|
|
2378
|
+
width: '1.25rem',
|
|
2379
|
+
height: '1.25rem',
|
|
2380
|
+
},
|
|
2381
|
+
},
|
|
2382
|
+
status: {
|
|
2383
|
+
default: {
|
|
2384
|
+
'&:checked, &:indeterminate': {
|
|
2385
|
+
borderColor: c.primaryMain,
|
|
2386
|
+
backgroundColor: c.primaryMain,
|
|
2387
|
+
},
|
|
2388
|
+
},
|
|
2389
|
+
error: {
|
|
2390
|
+
borderColor: c.errorMain,
|
|
2391
|
+
'&:checked, &:indeterminate': {
|
|
2392
|
+
borderColor: c.errorMain,
|
|
2393
|
+
backgroundColor: c.errorMain,
|
|
2394
|
+
},
|
|
2395
|
+
},
|
|
2396
|
+
success: {
|
|
2397
|
+
borderColor: c.successMain,
|
|
2398
|
+
'&:checked, &:indeterminate': {
|
|
2399
|
+
borderColor: c.successMain,
|
|
2400
|
+
backgroundColor: c.successMain,
|
|
2401
|
+
},
|
|
2402
|
+
},
|
|
2403
|
+
warning: {
|
|
2404
|
+
borderColor: c.warningMain,
|
|
2405
|
+
'&:checked, &:indeterminate': {
|
|
2406
|
+
borderColor: c.warningMain,
|
|
2407
|
+
backgroundColor: c.warningMain,
|
|
2408
|
+
},
|
|
2409
|
+
},
|
|
2410
|
+
},
|
|
2411
|
+
disabled: {
|
|
2412
|
+
true: {
|
|
2413
|
+
cursor: 'not-allowed',
|
|
2414
|
+
backgroundColor: c.disabledMain,
|
|
2415
|
+
borderColor: c.disabledMain,
|
|
2416
|
+
'&:checked, &:indeterminate': {
|
|
2417
|
+
backgroundColor: c.disabledText,
|
|
2418
|
+
borderColor: c.disabledText,
|
|
2419
|
+
},
|
|
2420
|
+
},
|
|
2421
|
+
false: {},
|
|
2422
|
+
},
|
|
2423
|
+
},
|
|
2424
|
+
defaultVariants: {
|
|
2425
|
+
size: 'md',
|
|
2426
|
+
status: 'default',
|
|
2427
|
+
disabled: 'false',
|
|
2428
|
+
},
|
|
2429
|
+
};
|
|
2430
|
+
}, { id: 'checkbox-input' });
|
|
2431
|
+
const CHECKBOX_STYLES = createStyles((theme) => ({
|
|
2432
|
+
wrapper: {
|
|
2433
|
+
display: 'inline-flex',
|
|
2434
|
+
flexDirection: 'column',
|
|
2435
|
+
gap: theme.spacing.xs,
|
|
2436
|
+
},
|
|
2437
|
+
helper: {
|
|
2438
|
+
marginLeft: `calc(1.125rem + ${theme.spacing.sm})`,
|
|
2439
|
+
},
|
|
2440
|
+
}), { id: 'checkbox-extra' });
|
|
2441
|
+
|
|
2442
|
+
/** Handles id generation, ref merging and native indeterminate state sync. */
|
|
2443
|
+
const useCheckbox = ({ id, ref, indeterminate = false }) => {
|
|
2444
|
+
const generatedId = useId();
|
|
2445
|
+
const checkboxId = id ?? generatedId;
|
|
2446
|
+
const helperId = `${checkboxId}-helper`;
|
|
2447
|
+
const inputRef = useRef(null);
|
|
2448
|
+
const mergedRef = useMergedRefs(ref, inputRef);
|
|
2449
|
+
useEffect(() => {
|
|
2450
|
+
if (inputRef.current) {
|
|
2451
|
+
inputRef.current.indeterminate = indeterminate;
|
|
2452
|
+
}
|
|
2453
|
+
}, [indeterminate]);
|
|
2454
|
+
return {
|
|
2455
|
+
checkboxId,
|
|
2456
|
+
helperId,
|
|
2457
|
+
mergedRef,
|
|
2458
|
+
inputRef,
|
|
2459
|
+
};
|
|
2460
|
+
};
|
|
2461
|
+
|
|
2462
|
+
const Checkbox = ({ ref, label, helperText, size = 'md', status = 'default', indeterminate = false, error, id, disabled, required, ...rest }) => {
|
|
2463
|
+
const resolvedStatus = error ? 'error' : status;
|
|
2464
|
+
const { checkboxId, helperId, mergedRef } = useCheckbox({ id, ref, indeterminate });
|
|
2465
|
+
return (jsxs("div", { className: CHECKBOX_STYLES.wrapper, children: [jsxs("label", { htmlFor: checkboxId, className: CHECKBOX_ROOT_VARIANTS({ disabled: disabled ? 'true' : 'false' }), children: [jsx("input", { ref: mergedRef, id: checkboxId, type: 'checkbox', disabled: disabled, required: required, "aria-required": required || undefined, "aria-invalid": resolvedStatus === 'error' || undefined, "aria-describedby": helperText !== undefined ? helperId : undefined, "aria-errormessage": resolvedStatus === 'error' && helperText !== undefined ? helperId : undefined, className: CHECKBOX_INPUT_VARIANTS({
|
|
2466
|
+
size,
|
|
2467
|
+
status: resolvedStatus,
|
|
2468
|
+
disabled: disabled ? 'true' : 'false',
|
|
2469
|
+
}), ...rest }), label !== undefined && (jsx(Text, { variant: 'span', fontSize: size === 'lg' ? 'md' : 'sm', color: disabled ? 'textDisabled' : 'textSecondary', children: label }))] }), helperText !== undefined && (jsx(FormHelperText, { id: helperId, status: resolvedStatus, className: CHECKBOX_STYLES.helper, children: helperText }))] }));
|
|
2470
|
+
};
|
|
2471
|
+
Checkbox.displayName = 'Checkbox';
|
|
2472
|
+
|
|
2260
2473
|
const CARD_VARIANTS = createVariants((theme) => ({
|
|
2261
2474
|
base: {
|
|
2262
2475
|
boxSizing: 'border-box',
|
|
@@ -2340,6 +2553,361 @@ const Grid = ({ display = 'grid', columns, rows, autoFlow, autoColumns, autoRows
|
|
|
2340
2553
|
};
|
|
2341
2554
|
Grid.displayName = 'Grid';
|
|
2342
2555
|
|
|
2556
|
+
const DrawerContext = createContext({
|
|
2557
|
+
isExpanded: true,
|
|
2558
|
+
});
|
|
2559
|
+
const useDrawerContext = () => useContext(DrawerContext);
|
|
2560
|
+
|
|
2561
|
+
const TRANSITION$2 = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
|
|
2562
|
+
const DRAWER_STYLES = createStyles((theme) => ({
|
|
2563
|
+
root: ({ isExpanded }) => ({
|
|
2564
|
+
display: 'flex',
|
|
2565
|
+
flexDirection: 'column',
|
|
2566
|
+
width: isExpanded ? EXPANDED_DRAWER_WIDTH : COLLAPSED_DRAWER_WIDTH,
|
|
2567
|
+
transition: `width ${theme.transition.slow}`,
|
|
2568
|
+
overflow: 'hidden',
|
|
2569
|
+
backgroundColor: theme.colors.surfacePaper,
|
|
2570
|
+
borderRight: `1px solid ${theme.colors.borderMain}`,
|
|
2571
|
+
boxSizing: 'border-box',
|
|
2572
|
+
flexShrink: 0,
|
|
2573
|
+
}),
|
|
2574
|
+
/** Temporary variant: slides in from the left as a fixed portal overlay. */
|
|
2575
|
+
temporaryPanel: {
|
|
2576
|
+
position: 'fixed',
|
|
2577
|
+
top: 0,
|
|
2578
|
+
left: 0,
|
|
2579
|
+
bottom: 0,
|
|
2580
|
+
width: EXPANDED_DRAWER_WIDTH,
|
|
2581
|
+
zIndex: theme.zIndex.modal,
|
|
2582
|
+
display: 'flex',
|
|
2583
|
+
flexDirection: 'column',
|
|
2584
|
+
backgroundColor: theme.colors.surfacePaper,
|
|
2585
|
+
borderRight: `1px solid ${theme.colors.borderMain}`,
|
|
2586
|
+
boxSizing: 'border-box',
|
|
2587
|
+
overflowY: 'auto',
|
|
2588
|
+
overflowX: 'hidden',
|
|
2589
|
+
willChange: 'transform',
|
|
2590
|
+
transform: 'translateX(-100%)',
|
|
2591
|
+
transition: `transform ${TRANSITION$2}`,
|
|
2592
|
+
boxShadow: theme.shadows.xl,
|
|
2593
|
+
},
|
|
2594
|
+
temporaryPanelVisible: {
|
|
2595
|
+
transform: 'translateX(0)',
|
|
2596
|
+
},
|
|
2597
|
+
}));
|
|
2598
|
+
|
|
2599
|
+
/**
|
|
2600
|
+
* Responsive breakpoints (min-width, mobile-first).
|
|
2601
|
+
* Keep in sync with themeBreakpoints.
|
|
2602
|
+
*/
|
|
2603
|
+
const BREAKPOINTS = {
|
|
2604
|
+
sm: 640};
|
|
2605
|
+
/** Max-width media query strings (max = breakpoint - 1px). */
|
|
2606
|
+
const MEDIA_MAX = {
|
|
2607
|
+
sm: `max-width: ${BREAKPOINTS.sm - 1}px`};
|
|
2608
|
+
|
|
2609
|
+
const MOBILE_MQ = `(max-width: ${BREAKPOINTS.sm - 1}px)`;
|
|
2610
|
+
/**
|
|
2611
|
+
* Resolves the effective drawer variant based on the explicit `variant` prop
|
|
2612
|
+
* and the current viewport width.
|
|
2613
|
+
*
|
|
2614
|
+
* - If `variant` is explicitly provided (`'permanent'` or `'temporary'`), returns it as-is.
|
|
2615
|
+
* - Otherwise, auto-detects: `'temporary'` on mobile (< sm breakpoint), `'permanent'` on desktop.
|
|
2616
|
+
*
|
|
2617
|
+
* Reacts to viewport changes (window resize) so switching between mobile and desktop
|
|
2618
|
+
* automatically updates the variant without requiring a page reload.
|
|
2619
|
+
*/
|
|
2620
|
+
const useDrawerVariant = (variant) => {
|
|
2621
|
+
const [isMobile, setIsMobile] = useState(() => {
|
|
2622
|
+
if (typeof window === 'undefined') {
|
|
2623
|
+
return false;
|
|
2624
|
+
}
|
|
2625
|
+
return window.matchMedia(MOBILE_MQ).matches;
|
|
2626
|
+
});
|
|
2627
|
+
useEffect(() => {
|
|
2628
|
+
if (typeof window === 'undefined') {
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
const mq = window.matchMedia(MOBILE_MQ);
|
|
2632
|
+
const handler = (e) => setIsMobile(e.matches);
|
|
2633
|
+
mq.addEventListener('change', handler);
|
|
2634
|
+
return () => mq.removeEventListener('change', handler);
|
|
2635
|
+
}, []);
|
|
2636
|
+
if (variant !== undefined) {
|
|
2637
|
+
return variant;
|
|
2638
|
+
}
|
|
2639
|
+
return isMobile ? 'temporary' : 'permanent';
|
|
2640
|
+
};
|
|
2641
|
+
|
|
2642
|
+
const TRANSITION$1 = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
|
|
2643
|
+
const BACKDROP_STYLES = createStyles((theme) => ({
|
|
2644
|
+
root: {
|
|
2645
|
+
position: 'fixed',
|
|
2646
|
+
inset: 0,
|
|
2647
|
+
zIndex: theme.zIndex.modal - 1,
|
|
2648
|
+
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
2649
|
+
transition: `background-color ${TRANSITION$1}`,
|
|
2650
|
+
},
|
|
2651
|
+
visible: {
|
|
2652
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
2653
|
+
},
|
|
2654
|
+
}), { id: 'backdrop' });
|
|
2655
|
+
|
|
2656
|
+
/**
|
|
2657
|
+
* Semi-transparent full-screen overlay used to visually block page content
|
|
2658
|
+
* while a modal element (dialog, temporary drawer…) is open.
|
|
2659
|
+
*
|
|
2660
|
+
* The `visible` prop drives the opacity transition: `false` = transparent,
|
|
2661
|
+
* `true` = `rgba(0,0,0,0.5)`. Mount/unmount is managed by the parent.
|
|
2662
|
+
*
|
|
2663
|
+
* @example
|
|
2664
|
+
* <Backdrop visible={isFadingIn} onClick={onClose} />
|
|
2665
|
+
*/
|
|
2666
|
+
const Backdrop = ({ visible, onClick }) => (jsx("div", { className: cx(BACKDROP_STYLES.root, visible && BACKDROP_STYLES.visible), onClick: onClick, "aria-hidden": true }));
|
|
2667
|
+
Backdrop.displayName = 'Backdrop';
|
|
2668
|
+
|
|
2669
|
+
/**
|
|
2670
|
+
* Manages mount/unmount transitions with a two-phase approach (visible → fading-in).
|
|
2671
|
+
*
|
|
2672
|
+
* Opening sequence:
|
|
2673
|
+
* 1. `useLayoutEffect` sets `isVisible=true` synchronously before the browser paints
|
|
2674
|
+
* → element is in the DOM at its initial hidden state on the very first frame.
|
|
2675
|
+
* 2. Double `requestAnimationFrame` in `useEffect` waits for two rendered frames
|
|
2676
|
+
* before setting `isFadingIn=true`, guaranteeing the CSS transition always starts
|
|
2677
|
+
* from the painted hidden state (prevents flickering).
|
|
2678
|
+
*
|
|
2679
|
+
* Closing sequence:
|
|
2680
|
+
* `isFadingIn=false` → CSS transition plays → after `duration` ms → `isVisible=false`.
|
|
2681
|
+
*
|
|
2682
|
+
* @param isOpen - Whether the element should be shown.
|
|
2683
|
+
* @param duration - Transition duration in milliseconds (defaults to `DEFAULT_TRANSITION_DURATION_MS`).
|
|
2684
|
+
*/
|
|
2685
|
+
const useTransitionRender = (isOpen, duration = DEFAULT_TRANSITION_DURATION_MS) => {
|
|
2686
|
+
const [isVisible, setIsVisible] = useState(isOpen);
|
|
2687
|
+
const [isFadingIn, setIsFadingIn] = useState(isOpen);
|
|
2688
|
+
// Mount synchronously before paint so the element is in the DOM at opacity:0
|
|
2689
|
+
// on the very first frame — no extra render cycle between null and the initial state.
|
|
2690
|
+
useLayoutEffect(() => {
|
|
2691
|
+
if (isOpen) {
|
|
2692
|
+
setIsVisible(true);
|
|
2693
|
+
}
|
|
2694
|
+
}, [isOpen]);
|
|
2695
|
+
useEffect(() => {
|
|
2696
|
+
if (isOpen) {
|
|
2697
|
+
// Double RAF: frame 1 → element rendered; frame 2 → element painted at hidden
|
|
2698
|
+
// state → THEN trigger the CSS transition.
|
|
2699
|
+
let raf2;
|
|
2700
|
+
const raf1 = requestAnimationFrame(() => {
|
|
2701
|
+
raf2 = requestAnimationFrame(() => setIsFadingIn(true));
|
|
2702
|
+
});
|
|
2703
|
+
return () => {
|
|
2704
|
+
cancelAnimationFrame(raf1);
|
|
2705
|
+
cancelAnimationFrame(raf2);
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
else {
|
|
2709
|
+
setIsFadingIn(false);
|
|
2710
|
+
const timeout = setTimeout(() => setIsVisible(false), duration);
|
|
2711
|
+
return () => clearTimeout(timeout);
|
|
2712
|
+
}
|
|
2713
|
+
}, [isOpen, duration]);
|
|
2714
|
+
return { isVisible, isFadingIn };
|
|
2715
|
+
};
|
|
2716
|
+
|
|
2717
|
+
/**
|
|
2718
|
+
* Locks scrolling on `document.body` while `active` is true.
|
|
2719
|
+
*
|
|
2720
|
+
* Preserves the current scroll position and keeps the scrollbar gutter
|
|
2721
|
+
* (`overflow-y: scroll`) to avoid horizontal layout shift when the scrollbar
|
|
2722
|
+
* disappears. The original styles and scroll position are restored on cleanup.
|
|
2723
|
+
*
|
|
2724
|
+
* @example useBodyScrollLock(isDialogOpen)
|
|
2725
|
+
*/
|
|
2726
|
+
const useBodyScrollLock = (active) => {
|
|
2727
|
+
useEffect(() => {
|
|
2728
|
+
if (!active) {
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
const scrollY = window.scrollY;
|
|
2732
|
+
const body = document.body;
|
|
2733
|
+
body.style.position = 'fixed';
|
|
2734
|
+
body.style.top = `-${scrollY}px`;
|
|
2735
|
+
body.style.overflowY = 'scroll';
|
|
2736
|
+
body.style.width = '100%';
|
|
2737
|
+
return () => {
|
|
2738
|
+
body.style.position = '';
|
|
2739
|
+
body.style.top = '';
|
|
2740
|
+
body.style.overflowY = '';
|
|
2741
|
+
body.style.width = '';
|
|
2742
|
+
window.scrollTo(0, scrollY);
|
|
2743
|
+
};
|
|
2744
|
+
}, [active]);
|
|
2745
|
+
};
|
|
2746
|
+
|
|
2747
|
+
const DrawerHeader = ({ children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }) => {
|
|
2748
|
+
return (jsx(Box, { px: 'sm', py: 'xs', role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...rest, children: children }));
|
|
2749
|
+
};
|
|
2750
|
+
|
|
2751
|
+
const DrawerBody = ({ children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }) => {
|
|
2752
|
+
return (jsx(Stack, { px: 'sm', py: 'xs', height: '100%', flexDirection: 'column', gap: 'none', role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, overflow: 'auto', ...rest, children: children }));
|
|
2753
|
+
};
|
|
2754
|
+
|
|
2755
|
+
const DrawerFooter = ({ children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }) => {
|
|
2756
|
+
return (jsx(Box, { px: 'sm', py: 'xs', role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...rest, children: children }));
|
|
2757
|
+
};
|
|
2758
|
+
|
|
2759
|
+
const DRAWER_ITEM_STYLES = createStyles((theme) => ({
|
|
2760
|
+
root: ({ selected, isExpanded }) => ({
|
|
2761
|
+
position: 'relative',
|
|
2762
|
+
display: 'flex',
|
|
2763
|
+
alignItems: 'center',
|
|
2764
|
+
width: isExpanded ? EXPANDED_DRAWER_ITEM_WIDTH : COLLAPSED_DRAWER_ITEM_WIDTH,
|
|
2765
|
+
gap: theme.spacing.sm,
|
|
2766
|
+
padding: `0 ${theme.spacing.smPlus}`,
|
|
2767
|
+
border: '1px solid transparent',
|
|
2768
|
+
borderRadius: theme.radius.md,
|
|
2769
|
+
fontFamily: 'inherit',
|
|
2770
|
+
fontSize: theme.fontSize.sm,
|
|
2771
|
+
userSelect: 'none',
|
|
2772
|
+
cursor: 'pointer',
|
|
2773
|
+
outline: 'none',
|
|
2774
|
+
textDecoration: 'none',
|
|
2775
|
+
boxSizing: 'border-box',
|
|
2776
|
+
transition: `width ${theme.transition.slow}, background-color ${theme.transition.fast}, color ${theme.transition.fast}`,
|
|
2777
|
+
whiteSpace: 'nowrap',
|
|
2778
|
+
overflow: 'hidden',
|
|
2779
|
+
height: DEFAULT_DRAWER_ITEM_SIZE,
|
|
2780
|
+
...(selected
|
|
2781
|
+
? {
|
|
2782
|
+
backgroundColor: theme.colors.primarySubtle,
|
|
2783
|
+
color: theme.colors.primaryMain,
|
|
2784
|
+
':hover': { backgroundColor: theme.colors.primarySubtleHover, color: theme.colors.primaryHover },
|
|
2785
|
+
':active': { backgroundColor: theme.colors.primarySubtleActive, color: theme.colors.primaryActive },
|
|
2786
|
+
}
|
|
2787
|
+
: {
|
|
2788
|
+
backgroundColor: 'transparent',
|
|
2789
|
+
color: theme.colors.defaultMain,
|
|
2790
|
+
':hover:not(:disabled)': { backgroundColor: theme.colors.defaultSubtleHover, color: theme.colors.defaultHover },
|
|
2791
|
+
':active:not(:disabled)': { backgroundColor: theme.colors.defaultSubtleActive, color: theme.colors.defaultActive },
|
|
2792
|
+
}),
|
|
2793
|
+
':focus-visible': { boxShadow: theme.shadows.focus },
|
|
2794
|
+
':disabled': { cursor: 'not-allowed', opacity: theme.opacity.high },
|
|
2795
|
+
}),
|
|
2796
|
+
iconWrap: {
|
|
2797
|
+
display: 'flex',
|
|
2798
|
+
alignItems: 'center',
|
|
2799
|
+
justifyContent: 'center',
|
|
2800
|
+
flexShrink: 0,
|
|
2801
|
+
},
|
|
2802
|
+
labelWrapper: ({ isFadingIn }) => ({
|
|
2803
|
+
display: 'flex',
|
|
2804
|
+
alignItems: 'center',
|
|
2805
|
+
flexDirection: 'row',
|
|
2806
|
+
gap: theme.spacing.sm,
|
|
2807
|
+
width: '100%',
|
|
2808
|
+
justifyContent: 'space-between',
|
|
2809
|
+
flex: 1,
|
|
2810
|
+
overflow: 'hidden',
|
|
2811
|
+
minWidth: 0,
|
|
2812
|
+
opacity: isFadingIn ? 1 : 0,
|
|
2813
|
+
transition: `opacity ${DEFAULT_TRANSITION_DURATION_MS}ms ease`,
|
|
2814
|
+
}),
|
|
2815
|
+
label: {
|
|
2816
|
+
flex: 1,
|
|
2817
|
+
overflow: 'hidden',
|
|
2818
|
+
textOverflow: 'ellipsis',
|
|
2819
|
+
fontWeight: theme.fontWeight.medium,
|
|
2820
|
+
},
|
|
2821
|
+
endContent: {
|
|
2822
|
+
display: 'flex',
|
|
2823
|
+
alignItems: 'center',
|
|
2824
|
+
flexShrink: 0,
|
|
2825
|
+
marginLeft: 'auto',
|
|
2826
|
+
},
|
|
2827
|
+
}));
|
|
2828
|
+
|
|
2829
|
+
/**
|
|
2830
|
+
* Navigation/action item for use inside Drawer.Body or Drawer.Footer.
|
|
2831
|
+
*
|
|
2832
|
+
* - In **expanded** mode: shows icon + label (fade-in) + optional end content.
|
|
2833
|
+
* - In **collapsed** mode: shows only the icon; the label fades out before unmounting
|
|
2834
|
+
* and appears as a right-side tooltip on hover.
|
|
2835
|
+
* - Renders as `<a>` when `href` is provided, otherwise as `<button>`.
|
|
2836
|
+
* - The `selected` prop applies a primary-color highlight.
|
|
2837
|
+
*/
|
|
2838
|
+
const DrawerItem = ({ startIcon, label, selected = false, endContent, href, onClick, disabled = false, ariaLabel, ariaLabelledBy, ariaDescribedBy, ariaControls, ariaExpanded, ariaHasPopup, ariaCurrent, }) => {
|
|
2839
|
+
const { isExpanded } = useDrawerContext();
|
|
2840
|
+
const { isVisible: isLabelVisible, isFadingIn: isLabelFadingIn } = useTransitionRender(isExpanded);
|
|
2841
|
+
const rootClassName = DRAWER_ITEM_STYLES.root({ selected, isExpanded });
|
|
2842
|
+
const computedAriaLabel = ariaLabel ?? (!isExpanded ? label : undefined);
|
|
2843
|
+
const computedAriaCurrent = ariaCurrent ?? (selected ? 'page' : undefined);
|
|
2844
|
+
const handleClick = (e) => {
|
|
2845
|
+
if (disabled) {
|
|
2846
|
+
e.preventDefault();
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
onClick?.();
|
|
2850
|
+
};
|
|
2851
|
+
const innerContent = (jsxs(Stack, { width: '100%', justifyContent: 'start', children: [jsx("span", { className: DRAWER_ITEM_STYLES.iconWrap, children: jsx(Icon, { icon: startIcon, size: 'md' }) }), isLabelVisible && (jsxs("span", { className: DRAWER_ITEM_STYLES.labelWrapper({ isFadingIn: isLabelFadingIn }), children: [jsx(Text, { variant: 'span', fontSize: 'sm', fontWeight: 'medium', className: DRAWER_ITEM_STYLES.label, textAlign: 'start', children: label }), endContent && (jsx("span", { className: DRAWER_ITEM_STYLES.endContent, children: endContent }))] }))] }));
|
|
2852
|
+
const item = href ? (jsx("a", { href: href, className: rootClassName, "aria-disabled": disabled || undefined, "aria-current": computedAriaCurrent, "aria-label": computedAriaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-controls": ariaControls, "aria-expanded": ariaExpanded, "aria-haspopup": ariaHasPopup, tabIndex: disabled ? -1 : undefined, onClick: handleClick, children: innerContent })) : (jsx("button", { type: 'button', className: rootClassName, disabled: disabled, "aria-current": computedAriaCurrent, "aria-label": computedAriaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-controls": ariaControls, "aria-expanded": ariaExpanded, "aria-haspopup": ariaHasPopup, onClick: handleClick, children: innerContent }));
|
|
2853
|
+
return (jsx(Tooltip, { label: label, placement: 'right', inline: true, withArrow: true, disabled: isExpanded || disabled, children: item }));
|
|
2854
|
+
};
|
|
2855
|
+
DrawerItem.displayName = 'DrawerItem';
|
|
2856
|
+
|
|
2857
|
+
/**
|
|
2858
|
+
* Internal component: renders the temporary drawer as a fixed portal overlay
|
|
2859
|
+
* that slides in from the left with a backdrop, animated via `useTransitionRender`.
|
|
2860
|
+
*/
|
|
2861
|
+
const DrawerTemporaryPanel = ({ isExpanded, onClose, children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, }) => {
|
|
2862
|
+
const { isVisible, isFadingIn } = useTransitionRender(isExpanded);
|
|
2863
|
+
useBodyScrollLock(isExpanded);
|
|
2864
|
+
useKeyPress({ Escape: onClose }, { enabled: isExpanded });
|
|
2865
|
+
if (!isVisible) {
|
|
2866
|
+
return null;
|
|
2867
|
+
}
|
|
2868
|
+
return createPortal(jsxs(Fragment$1, { children: [jsx(Backdrop, { visible: isFadingIn, onClick: onClose }), jsx(DrawerContext.Provider, { value: { isExpanded: true }, children: jsx("nav", { className: cx(DRAWER_STYLES.temporaryPanel, isFadingIn && DRAWER_STYLES.temporaryPanelVisible), role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, children: children }) })] }), document.body);
|
|
2869
|
+
};
|
|
2870
|
+
DrawerTemporaryPanel.displayName = 'DrawerTemporaryPanel';
|
|
2871
|
+
// ─── Main Drawer ─────────────────────────────────────────────────────────────
|
|
2872
|
+
/**
|
|
2873
|
+
* Collapsible side navigation drawer with controlled expanded/collapsed state.
|
|
2874
|
+
*
|
|
2875
|
+
* **Variants**
|
|
2876
|
+
* - `'permanent'` (default on desktop): inline drawer that pushes page content.
|
|
2877
|
+
* Toggles between `expanded` and `collapsed` states with animated width.
|
|
2878
|
+
* - `'temporary'` (default on mobile): portal overlay with a backdrop that slides
|
|
2879
|
+
* in from the left. `isExpanded` controls open/closed; `onClose` is called on
|
|
2880
|
+
* backdrop click or Escape.
|
|
2881
|
+
* - Omit `variant` to auto-detect based on viewport width.
|
|
2882
|
+
*
|
|
2883
|
+
* @example Permanent
|
|
2884
|
+
* <Drawer isExpanded={open} onExpandedChange={setOpen} height="100dvh">
|
|
2885
|
+
* <Drawer.Header>…</Drawer.Header>
|
|
2886
|
+
* <Drawer.Body>
|
|
2887
|
+
* <Drawer.Item startIcon={HomeIcon} label="Home" selected />
|
|
2888
|
+
* </Drawer.Body>
|
|
2889
|
+
* </Drawer>
|
|
2890
|
+
*
|
|
2891
|
+
* @example Temporary
|
|
2892
|
+
* <Drawer variant="temporary" isExpanded={open} onClose={() => setOpen(false)}>
|
|
2893
|
+
* …
|
|
2894
|
+
* </Drawer>
|
|
2895
|
+
*/
|
|
2896
|
+
const DrawerBase = ({ height = '100dvh', isExpanded, onExpandedChange, onClose, variant, children, role = 'navigation', ariaLabel = 'Navigation', ariaLabelledBy, ariaDescribedBy, }) => {
|
|
2897
|
+
const resolvedVariant = useDrawerVariant(variant);
|
|
2898
|
+
const handleClose = onClose ?? (() => onExpandedChange?.(false));
|
|
2899
|
+
if (resolvedVariant === 'temporary') {
|
|
2900
|
+
return (jsx(DrawerTemporaryPanel, { isExpanded: isExpanded, onClose: handleClose, height: height, role: role, ariaLabel: ariaLabel, ariaLabelledBy: ariaLabelledBy, ariaDescribedBy: ariaDescribedBy, children: children }));
|
|
2901
|
+
}
|
|
2902
|
+
return (jsx(DrawerContext.Provider, { value: { isExpanded }, children: jsx("nav", { className: DRAWER_STYLES.root({ isExpanded }), style: { height }, role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, children: children }) }));
|
|
2903
|
+
};
|
|
2904
|
+
DrawerBase.displayName = 'Drawer';
|
|
2905
|
+
const Drawer = DrawerBase;
|
|
2906
|
+
Drawer.Header = DrawerHeader;
|
|
2907
|
+
Drawer.Body = DrawerBody;
|
|
2908
|
+
Drawer.Footer = DrawerFooter;
|
|
2909
|
+
Drawer.Item = DrawerItem;
|
|
2910
|
+
|
|
2343
2911
|
const AlertContext = createContext({
|
|
2344
2912
|
variant: 'default',
|
|
2345
2913
|
accentColor: 'defaultActive',
|
|
@@ -2392,6 +2960,15 @@ const VARIANT_ARIA_LIVE = {
|
|
|
2392
2960
|
warning: 'polite',
|
|
2393
2961
|
info: 'polite',
|
|
2394
2962
|
};
|
|
2963
|
+
/** role="alert" implies aria-live="assertive" — reserve it for errors.
|
|
2964
|
+
* Other variants use role="status" (implies aria-live="polite"). */
|
|
2965
|
+
const VARIANT_ROLE = {
|
|
2966
|
+
default: 'status',
|
|
2967
|
+
success: 'status',
|
|
2968
|
+
error: 'alert',
|
|
2969
|
+
warning: 'status',
|
|
2970
|
+
info: 'status',
|
|
2971
|
+
};
|
|
2395
2972
|
/**
|
|
2396
2973
|
* Inline alert banner with 5 visual variants: default, success, error, warning, info.
|
|
2397
2974
|
* Use `Alert.Title` (icon + heading) and `Alert.Body` (message) as children.
|
|
@@ -2404,38 +2981,15 @@ const VARIANT_ARIA_LIVE = {
|
|
|
2404
2981
|
*/
|
|
2405
2982
|
const AlertBase = ({ variant = 'default', children, width = '100%', outline = false, shadow = 'none', }) => {
|
|
2406
2983
|
const { backgroundColor, borderColor, accentColor } = VARIANT_TOKENS[variant];
|
|
2407
|
-
return (jsx(AlertContext.Provider, { value: { variant, accentColor }, children: jsx(Stack, { role:
|
|
2984
|
+
return (jsx(AlertContext.Provider, { value: { variant, accentColor }, children: jsx(Stack, { role: VARIANT_ROLE[variant], "aria-live": VARIANT_ARIA_LIVE[variant], flexDirection: 'column', gap: 'xs', padding: 'md', borderRadius: 'lg', backgroundColor: backgroundColor, borderColor: outline ? borderColor : undefined, borderWidth: outline ? '1px' : undefined, borderStyle: outline ? 'solid' : undefined, boxShadow: shadow, width: width, children: children }) }));
|
|
2408
2985
|
};
|
|
2409
2986
|
AlertBase.displayName = 'Alert';
|
|
2410
2987
|
const Alert = AlertBase;
|
|
2411
2988
|
Alert.Title = AlertTitle;
|
|
2412
2989
|
Alert.Body = AlertBody;
|
|
2413
2990
|
|
|
2414
|
-
/**
|
|
2415
|
-
* Responsive breakpoints (min-width, mobile-first).
|
|
2416
|
-
* Keep in sync with themeBreakpoints.
|
|
2417
|
-
*/
|
|
2418
|
-
const BREAKPOINTS = {
|
|
2419
|
-
sm: 640};
|
|
2420
|
-
/** Max-width media query strings (max = breakpoint - 1px). */
|
|
2421
|
-
const MEDIA_MAX = {
|
|
2422
|
-
sm: `max-width: ${BREAKPOINTS.sm - 1}px`};
|
|
2423
|
-
|
|
2424
|
-
/** Default duration in milliseconds for mount/unmount transition animations. */
|
|
2425
|
-
const DEFAULT_TRANSITION_DURATION_MS = 250;
|
|
2426
|
-
|
|
2427
2991
|
const TRANSITION = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
|
|
2428
2992
|
const DIALOG_STYLES = createStyles((theme) => ({
|
|
2429
|
-
backdrop: {
|
|
2430
|
-
position: 'fixed',
|
|
2431
|
-
inset: 0,
|
|
2432
|
-
zIndex: theme.zIndex.modal - 1,
|
|
2433
|
-
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
2434
|
-
transition: `background-color ${TRANSITION}`,
|
|
2435
|
-
},
|
|
2436
|
-
backdropVisible: {
|
|
2437
|
-
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
2438
|
-
},
|
|
2439
2993
|
panel: {
|
|
2440
2994
|
position: 'fixed',
|
|
2441
2995
|
inset: 0,
|
|
@@ -2493,84 +3047,6 @@ const DialogContext = createContext({
|
|
|
2493
3047
|
CloseIconComponent: null,
|
|
2494
3048
|
});
|
|
2495
3049
|
|
|
2496
|
-
/**
|
|
2497
|
-
* Manages mount/unmount transitions with a two-phase approach (visible → fading-in).
|
|
2498
|
-
*
|
|
2499
|
-
* Opening sequence:
|
|
2500
|
-
* 1. `useLayoutEffect` sets `isVisible=true` synchronously before the browser paints
|
|
2501
|
-
* → element is in the DOM at its initial hidden state on the very first frame.
|
|
2502
|
-
* 2. Double `requestAnimationFrame` in `useEffect` waits for two rendered frames
|
|
2503
|
-
* before setting `isFadingIn=true`, guaranteeing the CSS transition always starts
|
|
2504
|
-
* from the painted hidden state (prevents flickering).
|
|
2505
|
-
*
|
|
2506
|
-
* Closing sequence:
|
|
2507
|
-
* `isFadingIn=false` → CSS transition plays → after `duration` ms → `isVisible=false`.
|
|
2508
|
-
*
|
|
2509
|
-
* @param isOpen - Whether the element should be shown.
|
|
2510
|
-
* @param duration - Transition duration in milliseconds (defaults to `DEFAULT_TRANSITION_DURATION_MS`).
|
|
2511
|
-
*/
|
|
2512
|
-
const useTransitionRender = (isOpen, duration = DEFAULT_TRANSITION_DURATION_MS) => {
|
|
2513
|
-
const [isVisible, setIsVisible] = useState(isOpen);
|
|
2514
|
-
const [isFadingIn, setIsFadingIn] = useState(isOpen);
|
|
2515
|
-
// Mount synchronously before paint so the element is in the DOM at opacity:0
|
|
2516
|
-
// on the very first frame — no extra render cycle between null and the initial state.
|
|
2517
|
-
useLayoutEffect(() => {
|
|
2518
|
-
if (isOpen) {
|
|
2519
|
-
setIsVisible(true);
|
|
2520
|
-
}
|
|
2521
|
-
}, [isOpen]);
|
|
2522
|
-
useEffect(() => {
|
|
2523
|
-
if (isOpen) {
|
|
2524
|
-
// Double RAF: frame 1 → element rendered; frame 2 → element painted at hidden
|
|
2525
|
-
// state → THEN trigger the CSS transition.
|
|
2526
|
-
let raf2;
|
|
2527
|
-
const raf1 = requestAnimationFrame(() => {
|
|
2528
|
-
raf2 = requestAnimationFrame(() => setIsFadingIn(true));
|
|
2529
|
-
});
|
|
2530
|
-
return () => {
|
|
2531
|
-
cancelAnimationFrame(raf1);
|
|
2532
|
-
cancelAnimationFrame(raf2);
|
|
2533
|
-
};
|
|
2534
|
-
}
|
|
2535
|
-
else {
|
|
2536
|
-
setIsFadingIn(false);
|
|
2537
|
-
const timeout = setTimeout(() => setIsVisible(false), duration);
|
|
2538
|
-
return () => clearTimeout(timeout);
|
|
2539
|
-
}
|
|
2540
|
-
}, [isOpen, duration]);
|
|
2541
|
-
return { isVisible, isFadingIn };
|
|
2542
|
-
};
|
|
2543
|
-
|
|
2544
|
-
/**
|
|
2545
|
-
* Locks scrolling on `document.body` while `active` is true.
|
|
2546
|
-
*
|
|
2547
|
-
* Preserves the current scroll position and keeps the scrollbar gutter
|
|
2548
|
-
* (`overflow-y: scroll`) to avoid horizontal layout shift when the scrollbar
|
|
2549
|
-
* disappears. The original styles and scroll position are restored on cleanup.
|
|
2550
|
-
*
|
|
2551
|
-
* @example useBodyScrollLock(isDialogOpen)
|
|
2552
|
-
*/
|
|
2553
|
-
const useBodyScrollLock = (active) => {
|
|
2554
|
-
useEffect(() => {
|
|
2555
|
-
if (!active) {
|
|
2556
|
-
return;
|
|
2557
|
-
}
|
|
2558
|
-
const scrollY = window.scrollY;
|
|
2559
|
-
const body = document.body;
|
|
2560
|
-
body.style.position = 'fixed';
|
|
2561
|
-
body.style.top = `-${scrollY}px`;
|
|
2562
|
-
body.style.overflowY = 'scroll';
|
|
2563
|
-
body.style.width = '100%';
|
|
2564
|
-
return () => {
|
|
2565
|
-
body.style.position = '';
|
|
2566
|
-
body.style.top = '';
|
|
2567
|
-
body.style.overflowY = '';
|
|
2568
|
-
body.style.width = '';
|
|
2569
|
-
window.scrollTo(0, scrollY);
|
|
2570
|
-
};
|
|
2571
|
-
}, [active]);
|
|
2572
|
-
};
|
|
2573
|
-
|
|
2574
3050
|
const FOCUSABLE_SELECTOR = [
|
|
2575
3051
|
'a[href]',
|
|
2576
3052
|
'button:not([disabled])',
|
|
@@ -2672,7 +3148,7 @@ const useDialog = ({ open, onClose, closeOnBackdropClick, maxWidth, maxHeight, m
|
|
|
2672
3148
|
*/
|
|
2673
3149
|
const DialogHeader = ({ title, onClose }) => {
|
|
2674
3150
|
const { titleId, CloseIconComponent } = useContext(DialogContext);
|
|
2675
|
-
return (jsxs(Stack, { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 'md', paddingTop: 'md', paddingBottom: 'md', paddingLeft: 'lg', paddingRight: 'md', flexShrink: 0, children: [jsx(Text, { id: titleId, variant: 'span', fontSize: 'md', fontWeight: 'semibold', color: 'textPrimary', children: title }), jsx(IconButton, { icon: CloseIconComponent, ariaLabel: 'Close dialog', variant: 'text', color: 'neutral', size: 'sm', type: 'button', onClick: onClose })] }));
|
|
3151
|
+
return (jsxs(Stack, { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 'md', paddingTop: 'md', paddingBottom: 'md', paddingLeft: 'lg', paddingRight: 'md', flexShrink: 0, children: [jsx(Text, { id: titleId, variant: 'span', as: 'h2', fontSize: 'md', fontWeight: 'semibold', color: 'textPrimary', children: title }), jsx(IconButton, { icon: CloseIconComponent, ariaLabel: 'Close dialog', variant: 'text', color: 'neutral', size: 'sm', type: 'button', onClick: onClose })] }));
|
|
2676
3152
|
};
|
|
2677
3153
|
DialogHeader.displayName = 'Dialog.Header';
|
|
2678
3154
|
|
|
@@ -2706,7 +3182,7 @@ const DialogBase = ({ open, onClose, children, closeOnBackdropClick = false, ful
|
|
|
2706
3182
|
if (!isVisible) {
|
|
2707
3183
|
return null;
|
|
2708
3184
|
}
|
|
2709
|
-
return createPortal(jsxs(Fragment$1, { children: [jsx(
|
|
3185
|
+
return createPortal(jsxs(Fragment$1, { children: [jsx(Backdrop, { visible: isFadingIn, onClick: handleBackdropClick }), jsx("div", { ref: panelRef, role: 'dialog', "aria-modal": true, "aria-labelledby": labelledBy, "aria-label": ariaLabel, tabIndex: -1, className: cx(DIALOG_STYLES.panel, isFadingIn && DIALOG_STYLES.panelVisible, fullscreen && DIALOG_STYLES.panelFullscreen), style: cssVars, children: jsx(DialogContext.Provider, { value: { titleId, CloseIconComponent: CloseIcon }, children: children }) })] }), document.body);
|
|
2710
3186
|
};
|
|
2711
3187
|
DialogBase.displayName = 'Dialog';
|
|
2712
3188
|
const Dialog = DialogBase;
|
|
@@ -2917,13 +3393,19 @@ const themeShadows = {
|
|
|
2917
3393
|
|
|
2918
3394
|
/**
|
|
2919
3395
|
* Default spacing tokens
|
|
3396
|
+
*
|
|
3397
|
+
* ⚠️ Token keys MUST stay CSS-custom-property safe (letters, digits, hyphens only).
|
|
3398
|
+
* They are turned into CSS variables (e.g. `smPlus` → `--theme-spacing-sm-plus`) by
|
|
3399
|
+
* the theme proxy, so characters like `+` would produce invalid variable names and
|
|
3400
|
+
* silently break any style that uses them.
|
|
2920
3401
|
*/
|
|
2921
3402
|
const themeSpacing = {
|
|
2922
3403
|
none: '0',
|
|
2923
3404
|
'2xs': '0.125rem', // 2px
|
|
2924
3405
|
xs: '0.25rem', // 4px
|
|
2925
|
-
|
|
3406
|
+
xsPlus: '0.375rem', // 6px
|
|
2926
3407
|
sm: '0.5rem', // 8px
|
|
3408
|
+
smPlus: '0.75rem', // 12px
|
|
2927
3409
|
md: '1rem', // 16px
|
|
2928
3410
|
lg: '1.5rem', // 24px
|
|
2929
3411
|
xl: '2rem', // 32px
|
|
@@ -2937,9 +3419,9 @@ const themeSpacing = {
|
|
|
2937
3419
|
* Default transition tokens
|
|
2938
3420
|
*/
|
|
2939
3421
|
const themeTransition = {
|
|
2940
|
-
fast: '150ms ease-out',
|
|
2941
|
-
normal: '250ms ease-out',
|
|
2942
|
-
slow: '350ms ease-out',
|
|
3422
|
+
fast: '150ms ease-in-out',
|
|
3423
|
+
normal: '250ms ease-in-out',
|
|
3424
|
+
slow: '350ms ease-in-out',
|
|
2943
3425
|
};
|
|
2944
3426
|
|
|
2945
3427
|
/**
|
|
@@ -3094,5 +3576,5 @@ const darkTheme = createTheme({
|
|
|
3094
3576
|
breakpoints: themeBreakpoints,
|
|
3095
3577
|
});
|
|
3096
3578
|
|
|
3097
|
-
export { Alert, Badge, Box, Button, Card, Dialog, Form, Grid, Icon, IconButton, InfoBubble, Link, Menu, Select, Skeleton, Stack, Switch, Text, TextField, Tooltip, darkTheme, lightTheme };
|
|
3579
|
+
export { Alert, Backdrop, Badge, Box, Button, Card, Checkbox, Dialog, Drawer, Form, Grid, Icon, IconButton, InfoBubble, Link, Menu, Select, Skeleton, Stack, Switch, Text, TextField, Tooltip, darkTheme, lightTheme, useDrawerContext };
|
|
3098
3580
|
//# sourceMappingURL=index.js.map
|