@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/cjs/index.js
CHANGED
|
@@ -181,24 +181,32 @@ const skeletonShimmerAnimation = theme.keyframes({
|
|
|
181
181
|
'100%': { backgroundPosition: '0% 50%' },
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
184
|
+
/** Default duration in milliseconds for mount/unmount transition animations. */
|
|
185
|
+
const DEFAULT_TRANSITION_DURATION_MS = 250;
|
|
186
|
+
const DEFAULT_BUTTON_HEIGHT = 40;
|
|
187
|
+
const DEFAULT_DRAWER_ITEM_SIZE = 40;
|
|
188
|
+
/** Drawer widths (px) — single source of truth for both the Drawer and its items. */
|
|
189
|
+
const EXPANDED_DRAWER_WIDTH = 240;
|
|
190
|
+
const COLLAPSED_DRAWER_WIDTH = 58;
|
|
191
|
+
/**
|
|
192
|
+
* Horizontal padding (px) applied by `Drawer.Body` on EACH side of its items
|
|
193
|
+
* (Box `px="sm"` → theme.spacing.sm = 0.5rem = 8px).
|
|
194
|
+
*/
|
|
195
|
+
const DRAWER_BODY_HORIZONTAL_PADDING = 8;
|
|
196
|
+
/**
|
|
197
|
+
* DrawerItem widths (px), always derived from the drawer width minus the body
|
|
198
|
+
* horizontal padding. Using explicit widths (instead of `width: 100%`) lets the
|
|
199
|
+
* item animate its own width in sync with the drawer for a smooth transition.
|
|
200
|
+
*/
|
|
201
|
+
const EXPANDED_DRAWER_ITEM_WIDTH = EXPANDED_DRAWER_WIDTH - DRAWER_BODY_HORIZONTAL_PADDING * 2; // 264
|
|
202
|
+
const COLLAPSED_DRAWER_ITEM_WIDTH = COLLAPSED_DRAWER_WIDTH - DRAWER_BODY_HORIZONTAL_PADDING * 2; // 44
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Returns the complete root styles for an action button (base + color/variant),
|
|
206
|
+
* all in one object so CSS transitions work correctly.
|
|
207
|
+
* Size-specific styles (height, padding, fontSize) are added by the caller.
|
|
208
|
+
*/
|
|
209
|
+
const buildActionButtonRootStyle = (theme, variant, color) => {
|
|
202
210
|
const c = theme.colors;
|
|
203
211
|
const intents = {
|
|
204
212
|
primary: {
|
|
@@ -237,57 +245,60 @@ const buildActionButtonCompoundVariants = (theme) => {
|
|
|
237
245
|
fg: c.errorMain, fgHover: c.errorHover, border: c.errorMain,
|
|
238
246
|
},
|
|
239
247
|
};
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
};
|
|
248
|
+
const intent = intents[color];
|
|
249
|
+
const colorVariantStyles = variant === 'contained'
|
|
250
|
+
? {
|
|
251
|
+
backgroundColor: intent.main,
|
|
252
|
+
borderColor: intent.main,
|
|
253
|
+
color: intent.on,
|
|
254
|
+
boxShadow: theme.shadows.xs,
|
|
255
|
+
':hover:not(:disabled)': { backgroundColor: intent.hover, borderColor: intent.hover, boxShadow: theme.shadows.sm },
|
|
256
|
+
':active:not(:disabled)': { backgroundColor: intent.active, borderColor: intent.active, boxShadow: theme.shadows.none },
|
|
250
257
|
}
|
|
251
|
-
|
|
252
|
-
|
|
258
|
+
: variant === 'outlined'
|
|
259
|
+
? {
|
|
253
260
|
backgroundColor: 'transparent',
|
|
254
261
|
borderColor: intent.border,
|
|
255
262
|
color: intent.fg,
|
|
256
263
|
':hover:not(:disabled)': { backgroundColor: intent.subtleHover, color: intent.fgHover },
|
|
257
264
|
':active:not(:disabled)': { backgroundColor: intent.subtleActive, borderColor: intent.active, color: intent.active },
|
|
265
|
+
}
|
|
266
|
+
: {
|
|
267
|
+
backgroundColor: 'transparent',
|
|
268
|
+
borderColor: 'transparent',
|
|
269
|
+
color: intent.fg,
|
|
270
|
+
':hover:not(:disabled)': { backgroundColor: intent.subtleHover, color: intent.fgHover },
|
|
271
|
+
':active:not(:disabled)': { backgroundColor: intent.subtleActive, color: intent.active },
|
|
258
272
|
};
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
273
|
+
return {
|
|
274
|
+
position: 'relative',
|
|
275
|
+
display: 'inline-flex',
|
|
276
|
+
alignItems: 'center',
|
|
277
|
+
justifyContent: 'center',
|
|
278
|
+
boxSizing: 'border-box',
|
|
279
|
+
border: '1px solid transparent',
|
|
280
|
+
borderRadius: theme.radius.md,
|
|
281
|
+
fontFamily: 'inherit',
|
|
282
|
+
userSelect: 'none',
|
|
283
|
+
height: DEFAULT_BUTTON_HEIGHT,
|
|
284
|
+
cursor: 'pointer',
|
|
285
|
+
outline: 'none',
|
|
286
|
+
transition: `background-color ${theme.transition.normal}, border-color ${theme.transition.normal}, color ${theme.transition.normal}, box-shadow ${theme.transition.normal}`,
|
|
287
|
+
...colorVariantStyles,
|
|
288
|
+
':focus-visible': { boxShadow: theme.shadows.focus },
|
|
289
|
+
':disabled': { cursor: 'not-allowed', opacity: theme.opacity.high, boxShadow: 'none' },
|
|
267
290
|
};
|
|
268
|
-
return Object.keys(intents).flatMap((color) => APPEARANCES.map((appearance) => ({
|
|
269
|
-
color,
|
|
270
|
-
variant: appearance,
|
|
271
|
-
styles: appearanceStyles(intents[color], appearance),
|
|
272
|
-
})));
|
|
273
291
|
};
|
|
274
292
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
size
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
variant: { contained: {}, outlined: {}, text: {} },
|
|
285
|
-
color: { primary: {}, secondary: {}, neutral: {}, info: {}, success: {}, warning: {}, error: {} },
|
|
286
|
-
},
|
|
287
|
-
defaultVariants: { size: 'md', variant: 'contained', color: 'primary' },
|
|
288
|
-
compoundVariants: buildActionButtonCompoundVariants(theme),
|
|
289
|
-
}), { id: 'button' });
|
|
290
|
-
const BUTTON_STYLES = theme.createStyles({
|
|
293
|
+
const BUTTON_STYLES = theme.createStyles((theme) => ({
|
|
294
|
+
root: ({ variant, color, size }) => ({
|
|
295
|
+
...buildActionButtonRootStyle(theme, variant, color),
|
|
296
|
+
...(size === 'sm'
|
|
297
|
+
? { height: '2rem', padding: `0 ${theme.spacing.sm}`, fontSize: theme.fontSize.xs }
|
|
298
|
+
: size === 'lg'
|
|
299
|
+
? { height: '3rem', padding: `0 ${theme.spacing.lg}`, fontSize: theme.fontSize.md }
|
|
300
|
+
: { height: '2.5rem', padding: `0 ${theme.spacing.md}`, fontSize: theme.fontSize.sm }),
|
|
301
|
+
}),
|
|
291
302
|
/** Inner wrapper holding icons + label, centered by the button. */
|
|
292
303
|
content: {
|
|
293
304
|
display: 'inline-flex',
|
|
@@ -310,7 +321,14 @@ const BUTTON_STYLES = theme.createStyles({
|
|
|
310
321
|
animation: `${spinAnimation} 0.75s linear infinite`,
|
|
311
322
|
'@media (prefers-reduced-motion: reduce)': { animation: 'none' },
|
|
312
323
|
},
|
|
313
|
-
}, { id: 'button
|
|
324
|
+
}), { id: 'button' });
|
|
325
|
+
// Pre-generate CSS for all variant/color/size combinations at module load.
|
|
326
|
+
// This ensures the CSS is already in the stylesheet before the first user interaction,
|
|
327
|
+
// preventing the "first click is instant" issue caused by lazy CSS injection.
|
|
328
|
+
const BUTTON_VARIANT_VALUES = ['contained', 'outlined', 'text'];
|
|
329
|
+
const BUTTON_COLOR_VALUES = ['primary', 'secondary', 'neutral', 'info', 'success', 'warning', 'error'];
|
|
330
|
+
const BUTTON_SIZE_VALUES = ['sm', 'md', 'lg'];
|
|
331
|
+
BUTTON_VARIANT_VALUES.forEach(variant => BUTTON_COLOR_VALUES.forEach(color => BUTTON_SIZE_VALUES.forEach(size => BUTTON_STYLES.root({ variant, color, size }))));
|
|
314
332
|
|
|
315
333
|
const ICON_STYLES = theme.createStyles((theme) => ({
|
|
316
334
|
root: ({ size, strokeColor, fill, backgroundColor, padding, borderRadius }) => ({
|
|
@@ -576,35 +594,29 @@ const ICON_SIZE$2 = {
|
|
|
576
594
|
* @example <Button label='Delete' variant='outlined' color='error' startIcon={IconRegistry.CloseIcon} />
|
|
577
595
|
* @example <Button label='Submitting…' color='success' isLoading width='100%' />
|
|
578
596
|
*/
|
|
579
|
-
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 }) => {
|
|
597
|
+
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 }) => {
|
|
580
598
|
const isDisabled = disabled || isLoading;
|
|
581
599
|
const iconSize = ICON_SIZE$2[size];
|
|
582
|
-
const rootClassName =
|
|
600
|
+
const rootClassName = theme.cx(BUTTON_STYLES.root({ variant, color, size }), className);
|
|
583
601
|
const mergedStyle = {
|
|
584
602
|
...style,
|
|
585
603
|
...(width !== undefined ? { width } : {}),
|
|
586
604
|
...(flexGrow !== undefined ? { flexGrow } : {}),
|
|
587
605
|
...(flexShrink !== undefined ? { flexShrink } : {}),
|
|
588
606
|
};
|
|
589
|
-
return (jsxRuntime.jsxs("button", { ref: ref, type: type, className: rootClassName, disabled: isDisabled, "aria-busy": isLoading || undefined, style: mergedStyle, ...rest, children: [isLoading && (jsxRuntime.jsx("span", { className: BUTTON_STYLES.spinnerWrap, children: jsxRuntime.jsx(Icon, { icon: SpinnerIcon, size: iconSize, className: BUTTON_STYLES.spinnerIcon }) })), jsxRuntime.jsxs("span", { className: theme.cx(BUTTON_STYLES.content, isLoading && BUTTON_STYLES.contentHidden), children: [StartIcon && (jsxRuntime.jsx(Icon, { icon: StartIcon, size: iconSize })), label && (jsxRuntime.jsx(Text, { variant: 'span', fontSize: LABEL_FONT_SIZE$1[size], fontWeight: 'medium', lineHeight: 'none', children: label })), EndIcon && (jsxRuntime.jsx(Icon, { icon: EndIcon, size: iconSize }))] })] }));
|
|
607
|
+
return (jsxRuntime.jsxs("button", { ref: ref, type: type, className: rootClassName, disabled: isDisabled, "aria-busy": isLoading || undefined, style: mergedStyle, ...rest, children: [isLoading && (jsxRuntime.jsx("span", { className: BUTTON_STYLES.spinnerWrap, children: jsxRuntime.jsx(Icon, { icon: SpinnerIcon, size: iconSize, className: BUTTON_STYLES.spinnerIcon }) })), jsxRuntime.jsxs("span", { className: theme.cx(BUTTON_STYLES.content, isLoading && BUTTON_STYLES.contentHidden), children: [StartIcon && (jsxRuntime.jsx(Icon, { icon: StartIcon, size: iconSize })), (label !== undefined || children !== undefined) && (jsxRuntime.jsx(Text, { variant: 'span', fontSize: LABEL_FONT_SIZE$1[size], fontWeight: 'medium', lineHeight: 'none', children: label ?? children })), EndIcon && (jsxRuntime.jsx(Icon, { icon: EndIcon, size: iconSize }))] })] }));
|
|
590
608
|
};
|
|
591
609
|
Button.displayName = 'Button';
|
|
592
610
|
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
size
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
color: { primary: {}, secondary: {}, neutral: {}, info: {}, success: {}, warning: {}, error: {} },
|
|
603
|
-
},
|
|
604
|
-
defaultVariants: { size: 'md', variant: 'contained', color: 'primary' },
|
|
605
|
-
compoundVariants: buildActionButtonCompoundVariants(theme),
|
|
606
|
-
}), { id: 'icon-button' });
|
|
607
|
-
const ICON_BUTTON_STYLES = theme.createStyles({
|
|
611
|
+
const ICON_BUTTON_STYLES = theme.createStyles((theme) => ({
|
|
612
|
+
root: ({ variant, color, size }) => ({
|
|
613
|
+
...buildActionButtonRootStyle(theme, variant, color),
|
|
614
|
+
...(size === 'sm'
|
|
615
|
+
? { width: '2rem', height: '2rem', padding: '0' }
|
|
616
|
+
: size === 'lg'
|
|
617
|
+
? { width: '3rem', height: '3rem', padding: '0' }
|
|
618
|
+
: { width: '2.5rem', height: '2.5rem', padding: '0' }),
|
|
619
|
+
}),
|
|
608
620
|
/** Spinning animation applied to the SpinnerIcon. */
|
|
609
621
|
spinnerIcon: {
|
|
610
622
|
animation: `${spinAnimation} 0.75s linear infinite`,
|
|
@@ -620,7 +632,11 @@ const ICON_BUTTON_STYLES = theme.createStyles({
|
|
|
620
632
|
alignItems: 'center',
|
|
621
633
|
justifyContent: 'center',
|
|
622
634
|
},
|
|
623
|
-
}, { id: 'icon-button
|
|
635
|
+
}), { id: 'icon-button' });
|
|
636
|
+
const ICON_BUTTON_VARIANT_VALUES = ['contained', 'outlined', 'text'];
|
|
637
|
+
const ICON_BUTTON_COLOR_VALUES = ['primary', 'secondary', 'neutral', 'info', 'success', 'warning', 'error'];
|
|
638
|
+
const ICON_BUTTON_SIZE_VALUES = ['sm', 'md', 'lg'];
|
|
639
|
+
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 }))));
|
|
624
640
|
|
|
625
641
|
/** Maps the icon-button size to an Icon size token. */
|
|
626
642
|
const ICON_SIZE$1 = {
|
|
@@ -639,7 +655,7 @@ const ICON_SIZE$1 = {
|
|
|
639
655
|
const IconButton = ({ ref, icon: IconComponent, ariaLabel, variant = 'contained', color = 'primary', size = 'md', isLoading = false, className, type = 'button', disabled, ...rest }) => {
|
|
640
656
|
const isDisabled = disabled || isLoading;
|
|
641
657
|
const iconSize = ICON_SIZE$1[size];
|
|
642
|
-
const rootClassName =
|
|
658
|
+
const rootClassName = theme.cx(ICON_BUTTON_STYLES.root({ variant, color, size }), className);
|
|
643
659
|
return (jsxRuntime.jsxs("button", { ref: ref, type: type, className: rootClassName, disabled: isDisabled, "aria-label": ariaLabel, "aria-busy": isLoading || undefined, ...rest, children: [isLoading && (jsxRuntime.jsx("span", { className: ICON_BUTTON_STYLES.spinnerWrap, children: jsxRuntime.jsx(Icon, { icon: SpinnerIcon, size: iconSize, className: ICON_BUTTON_STYLES.spinnerIcon }) })), jsxRuntime.jsx(Icon, { icon: IconComponent, size: iconSize, className: theme.cx(isLoading && ICON_BUTTON_STYLES.iconHidden) })] }));
|
|
644
660
|
};
|
|
645
661
|
IconButton.displayName = 'IconButton';
|
|
@@ -687,12 +703,20 @@ const LINK_STYLES = theme.createStyles((theme) => ({
|
|
|
687
703
|
/**
|
|
688
704
|
* Theme-aware anchor element with optional icons and underline control.
|
|
689
705
|
*
|
|
706
|
+
* Supports SPA navigation (e.g. React Router) via `onClick` without `href`.
|
|
707
|
+
* In that case the component stays accessible: it gets `role="link"`,
|
|
708
|
+
* `tabIndex={0}` and keyboard Enter support automatically.
|
|
709
|
+
*
|
|
690
710
|
* @example <Link href='/about'>About</Link>
|
|
691
711
|
* @example <Link href='https://example.com' external>External site</Link>
|
|
692
712
|
* @example <Link href='/profile' underline='always' startIcon={UserIcon}>Profile</Link>
|
|
693
713
|
* @example <Link href='/terms' underline='none'>Terms</Link>
|
|
714
|
+
* @example <Link onClick={() => navigate('/about')}>About (SPA)</Link>
|
|
694
715
|
*/
|
|
695
|
-
const Link = ({ ref, underline = 'hover', external = false, disabled = false, startIcon: StartIcon, endIcon: EndIcon, children, className, onClick, onKeyDown, ...rest }) => {
|
|
716
|
+
const Link = ({ ref, underline = 'hover', external = false, disabled = false, startIcon: StartIcon, endIcon: EndIcon, children, className, href, onClick, onKeyDown, ...rest }) => {
|
|
717
|
+
// An <a> without href has no implicit ARIA role and is not focusable.
|
|
718
|
+
// When used for SPA navigation (onClick only), we restore both behaviours.
|
|
719
|
+
const hasHref = !!href;
|
|
696
720
|
const handleClick = (e) => {
|
|
697
721
|
if (disabled) {
|
|
698
722
|
e.preventDefault();
|
|
@@ -700,14 +724,22 @@ const Link = ({ ref, underline = 'hover', external = false, disabled = false, st
|
|
|
700
724
|
}
|
|
701
725
|
onClick?.(e);
|
|
702
726
|
};
|
|
703
|
-
// Prevents Enter navigation when disabled; satisfies jsx-a11y/click-events-have-key-events.
|
|
704
727
|
const handleKeyDown = (e) => {
|
|
705
728
|
if (disabled && e.key === 'Enter') {
|
|
706
729
|
e.preventDefault();
|
|
707
730
|
}
|
|
731
|
+
// Without href, the browser does not fire a click on Enter natively.
|
|
732
|
+
if (!hasHref && !disabled && e.key === 'Enter') {
|
|
733
|
+
e.currentTarget.click();
|
|
734
|
+
}
|
|
708
735
|
onKeyDown?.(e);
|
|
709
736
|
};
|
|
710
|
-
return (jsxRuntime.jsxs("a", { ref: ref, className: theme.cx(LINK_STYLES.root({ underline }), className), "aria-disabled": disabled || undefined,
|
|
737
|
+
return (jsxRuntime.jsxs("a", { ref: ref, href: href, className: theme.cx(LINK_STYLES.root({ underline }), className), "aria-disabled": disabled || undefined,
|
|
738
|
+
// Without href: must be explicitly put in the tab order.
|
|
739
|
+
// With href: the browser handles focusability natively (no tabIndex needed).
|
|
740
|
+
tabIndex: disabled ? -1 : (!hasHref ? 0 : undefined),
|
|
741
|
+
// Without href: <a> has no implicit ARIA role — add role="link" explicitly.
|
|
742
|
+
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' }) }))] }));
|
|
711
743
|
};
|
|
712
744
|
Link.displayName = 'Link';
|
|
713
745
|
|
|
@@ -855,7 +887,7 @@ const BADGE_VARIANTS = theme.createVariants((theme) => ({
|
|
|
855
887
|
size: {
|
|
856
888
|
sm: {
|
|
857
889
|
height: '1.25rem',
|
|
858
|
-
padding: `0.125rem ${theme.spacing
|
|
890
|
+
padding: `0.125rem ${theme.spacing.xsPlus}`,
|
|
859
891
|
fontSize: theme.fontSize['2xs'],
|
|
860
892
|
},
|
|
861
893
|
md: {
|
|
@@ -1585,6 +1617,18 @@ Box.displayName = 'Box';
|
|
|
1585
1617
|
const Stack = ({ flexDirection = 'row', display = 'flex', gap = 'sm', ...rest }) => (jsxRuntime.jsx(Box, { display: display, flexDirection: flexDirection, gap: gap, ...rest }));
|
|
1586
1618
|
Stack.displayName = 'Stack';
|
|
1587
1619
|
|
|
1620
|
+
const HELPER_COLOR_MAP = {
|
|
1621
|
+
default: 'textSecondary',
|
|
1622
|
+
error: 'errorHover',
|
|
1623
|
+
success: 'successHover',
|
|
1624
|
+
warning: 'warningHover',
|
|
1625
|
+
};
|
|
1626
|
+
const FormHelperText = ({ id, status = 'default', ariaLive, className, children, }) => {
|
|
1627
|
+
const resolvedAriaLive = ariaLive ?? (status === 'error' ? 'assertive' : 'polite');
|
|
1628
|
+
return (jsxRuntime.jsx(Text, { id: id, variant: 'span', fontSize: 'xs', color: HELPER_COLOR_MAP[status], "aria-live": resolvedAriaLive, className: className, children: children }));
|
|
1629
|
+
};
|
|
1630
|
+
FormHelperText.displayName = 'FormHelperText';
|
|
1631
|
+
|
|
1588
1632
|
const TEXTFIELD_WRAPPER_VARIANTS = theme.createVariants((theme) => {
|
|
1589
1633
|
const c = theme.colors;
|
|
1590
1634
|
return {
|
|
@@ -1707,18 +1751,11 @@ const ICON_BUTTON_SIZE_MAP = {
|
|
|
1707
1751
|
md: 'sm',
|
|
1708
1752
|
lg: 'md',
|
|
1709
1753
|
};
|
|
1710
|
-
/** Maps status to a theme color token used for the helper text. */
|
|
1711
|
-
const HELPER_COLOR_MAP$1 = {
|
|
1712
|
-
default: 'textSecondary',
|
|
1713
|
-
error: 'errorHover',
|
|
1714
|
-
success: 'successHover',
|
|
1715
|
-
warning: 'warningHover',
|
|
1716
|
-
};
|
|
1717
1754
|
/**
|
|
1718
1755
|
* Business logic for the TextField component: id resolution, ref merging,
|
|
1719
1756
|
* password visibility toggling and size/status derived tokens.
|
|
1720
1757
|
*/
|
|
1721
|
-
const useTextField = ({ id, ref, type, size,
|
|
1758
|
+
const useTextField = ({ id, ref, type, size, endAction, }) => {
|
|
1722
1759
|
const generatedId = React.useId();
|
|
1723
1760
|
const fieldId = id ?? generatedId;
|
|
1724
1761
|
const helperId = `${fieldId}-helper`;
|
|
@@ -1740,7 +1777,6 @@ const useTextField = ({ id, ref, type, size, status, endAction, }) => {
|
|
|
1740
1777
|
resolvedType,
|
|
1741
1778
|
iconSize: ICON_SIZE_MAP$1[size],
|
|
1742
1779
|
iconButtonSize: ICON_BUTTON_SIZE_MAP[size],
|
|
1743
|
-
helperColor: HELPER_COLOR_MAP$1[status],
|
|
1744
1780
|
hasEndSection: endAction !== undefined || isPassword,
|
|
1745
1781
|
focusInput,
|
|
1746
1782
|
};
|
|
@@ -1758,8 +1794,8 @@ const useTextField = ({ id, ref, type, size, status, endAction, }) => {
|
|
|
1758
1794
|
* @example <TextField label="Search" startIcon={SearchIcon} endAction={<ClearButton />} />
|
|
1759
1795
|
*/
|
|
1760
1796
|
const TextField = ({ ref, label, helperText, size = 'md', status = 'default', startIcon: StartIcon, endAction, type, id, disabled, required, ...rest }) => {
|
|
1761
|
-
const { fieldId, helperId, mergedRef, isPassword, showPassword, togglePassword, resolvedType, iconSize, iconButtonSize,
|
|
1762
|
-
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, 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, 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',
|
|
1797
|
+
const { fieldId, helperId, mergedRef, isPassword, showPassword, togglePassword, resolvedType, iconSize, iconButtonSize, hasEndSection, focusInput, } = useTextField({ id, ref, type, size, endAction });
|
|
1798
|
+
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 }))] }));
|
|
1763
1799
|
};
|
|
1764
1800
|
TextField.displayName = 'TextField';
|
|
1765
1801
|
|
|
@@ -1894,7 +1930,9 @@ const useMenuPosition = ({ anchorEl, open, menuRef, minWidth, gap = 4, }) => {
|
|
|
1894
1930
|
*/
|
|
1895
1931
|
const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
|
|
1896
1932
|
const panelRef = React.useRef(null);
|
|
1933
|
+
const baseId = React.useId();
|
|
1897
1934
|
const [focusedIndex, setFocusedIndex] = React.useState(-1);
|
|
1935
|
+
const [activeDescendant, setActiveDescendant] = React.useState(undefined);
|
|
1898
1936
|
const { style } = useMenuPosition({ anchorEl, open, menuRef: panelRef, minWidth });
|
|
1899
1937
|
/** Returns all non-disabled option elements inside the panel. */
|
|
1900
1938
|
const getOptions = React.useCallback(() => {
|
|
@@ -1917,22 +1955,31 @@ const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
|
|
|
1917
1955
|
});
|
|
1918
1956
|
return () => cancelAnimationFrame(raf);
|
|
1919
1957
|
}, [open, getOptions]);
|
|
1920
|
-
// Keep data-focused
|
|
1958
|
+
// Keep data-focused (visual highlight) and aria-activedescendant (screen reader
|
|
1959
|
+
// announcement) in sync with focusedIndex. Each option is assigned a stable id
|
|
1960
|
+
// so the listbox can reference the active one via aria-activedescendant.
|
|
1921
1961
|
React.useEffect(() => {
|
|
1922
1962
|
if (!open) {
|
|
1963
|
+
setActiveDescendant(undefined);
|
|
1923
1964
|
return;
|
|
1924
1965
|
}
|
|
1925
1966
|
const options = getOptions();
|
|
1967
|
+
let activeId;
|
|
1926
1968
|
options.forEach((el, idx) => {
|
|
1969
|
+
if (!el.id) {
|
|
1970
|
+
el.id = `${baseId}-option-${idx}`;
|
|
1971
|
+
}
|
|
1927
1972
|
if (idx === focusedIndex) {
|
|
1928
1973
|
el.setAttribute('data-focused', 'true');
|
|
1929
1974
|
el.scrollIntoView({ block: 'nearest' });
|
|
1975
|
+
activeId = el.id;
|
|
1930
1976
|
}
|
|
1931
1977
|
else {
|
|
1932
1978
|
el.removeAttribute('data-focused');
|
|
1933
1979
|
}
|
|
1934
1980
|
});
|
|
1935
|
-
|
|
1981
|
+
setActiveDescendant(activeId);
|
|
1982
|
+
}, [focusedIndex, open, getOptions, baseId]);
|
|
1936
1983
|
useKeyPress({
|
|
1937
1984
|
Escape: onClose,
|
|
1938
1985
|
ArrowDown: (e) => {
|
|
@@ -1970,7 +2017,7 @@ const useMenu = ({ open, onClose, anchorEl, minWidth }) => {
|
|
|
1970
2017
|
}
|
|
1971
2018
|
},
|
|
1972
2019
|
}, { enabled: open });
|
|
1973
|
-
return { panelRef, style };
|
|
2020
|
+
return { panelRef, style, activeDescendant };
|
|
1974
2021
|
};
|
|
1975
2022
|
|
|
1976
2023
|
const MENU_GROUP_STYLES = theme.createStyles((theme) => {
|
|
@@ -2011,7 +2058,8 @@ const MENU_GROUP_STYLES = theme.createStyles((theme) => {
|
|
|
2011
2058
|
}, { id: 'menu-group' });
|
|
2012
2059
|
|
|
2013
2060
|
const MenuGroup = ({ label, divider, children, }) => {
|
|
2014
|
-
|
|
2061
|
+
const labelId = React.useId();
|
|
2062
|
+
return (jsxRuntime.jsxs("div", { className: MENU_GROUP_STYLES.root, children: [divider && (jsxRuntime.jsx("div", { className: MENU_GROUP_STYLES.divider, role: 'separator', "aria-hidden": true })), label !== undefined && (jsxRuntime.jsx("span", { id: labelId, className: MENU_GROUP_STYLES.label, "aria-hidden": true, children: label })), jsxRuntime.jsx("ul", { className: MENU_GROUP_STYLES.list, role: 'group', "aria-labelledby": label !== undefined ? labelId : undefined, children: children })] }));
|
|
2015
2063
|
};
|
|
2016
2064
|
MenuGroup.displayName = 'MenuGroup';
|
|
2017
2065
|
|
|
@@ -2062,12 +2110,12 @@ const MenuItem = ({ ref, label, icon, selected, focused, disabled, onClick, ...r
|
|
|
2062
2110
|
};
|
|
2063
2111
|
MenuItem.displayName = 'MenuItem';
|
|
2064
2112
|
|
|
2065
|
-
const MenuBase = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', id, children, }) => {
|
|
2066
|
-
const { panelRef, style } = useMenu({ open, onClose, anchorEl, minWidth });
|
|
2113
|
+
const MenuBase = ({ open, onClose, anchorEl, minWidth, maxHeight = '20rem', id, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, children, }) => {
|
|
2114
|
+
const { panelRef, style, activeDescendant } = useMenu({ open, onClose, anchorEl, minWidth });
|
|
2067
2115
|
if (!open) {
|
|
2068
2116
|
return null;
|
|
2069
2117
|
}
|
|
2070
|
-
return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: MENU_STYLES.backdrop, onClick: onClose, "aria-hidden": true }), jsxRuntime.jsx("div", { ref: panelRef, id: id, role: 'listbox', tabIndex: -1, className: MENU_STYLES.panel, style: { ...style, maxHeight, outline: 'none' }, children: children })] }), document.body);
|
|
2118
|
+
return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: MENU_STYLES.backdrop, onClick: onClose, "aria-hidden": true }), jsxRuntime.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);
|
|
2071
2119
|
};
|
|
2072
2120
|
MenuBase.displayName = 'Menu';
|
|
2073
2121
|
const Menu = MenuBase;
|
|
@@ -2189,28 +2237,23 @@ const ICON_SIZE_MAP = {
|
|
|
2189
2237
|
md: 'md',
|
|
2190
2238
|
lg: 'lg',
|
|
2191
2239
|
};
|
|
2192
|
-
const SelectTrigger = ({ ref, size = 'md', status = 'default', open, hasValue, startIcon, disabled, children, ...rest }) => {
|
|
2240
|
+
const SelectTrigger = ({ ref, size = 'md', status = 'default', open, hasValue, startIcon, disabled, children, 'aria-expanded': ariaExpanded, 'aria-controls': ariaControls, ...rest }) => {
|
|
2193
2241
|
const iconSize = ICON_SIZE_MAP[size];
|
|
2194
|
-
return (jsxRuntime.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 && (jsxRuntime.jsx(Icon, { icon: startIcon, size: iconSize, strokeColor: 'textSecondary' })), jsxRuntime.jsx("span", { className: hasValue ? SELECT_TRIGGER_STYLES.value : SELECT_TRIGGER_STYLES.placeholder, children: children }), jsxRuntime.jsx(Icon, { icon: ChevronDownIcon, size: iconSize, className: theme.cx(SELECT_TRIGGER_STYLES.chevron, open ? SELECT_TRIGGER_STYLES.chevronOpen : undefined), strokeColor: 'textSecondary' })] }));
|
|
2242
|
+
return (jsxRuntime.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 && (jsxRuntime.jsx(Icon, { icon: startIcon, size: iconSize, strokeColor: 'textSecondary' })), jsxRuntime.jsx("span", { className: hasValue ? SELECT_TRIGGER_STYLES.value : SELECT_TRIGGER_STYLES.placeholder, children: children }), jsxRuntime.jsx(Icon, { icon: ChevronDownIcon, size: iconSize, className: theme.cx(SELECT_TRIGGER_STYLES.chevron, open ? SELECT_TRIGGER_STYLES.chevronOpen : undefined), strokeColor: 'textSecondary' })] }));
|
|
2195
2243
|
};
|
|
2196
2244
|
SelectTrigger.displayName = 'SelectTrigger';
|
|
2197
2245
|
|
|
2198
|
-
/** Maps status to a theme color token used for the helper text. */
|
|
2199
|
-
const HELPER_COLOR_MAP = {
|
|
2200
|
-
default: 'textSecondary',
|
|
2201
|
-
error: 'errorHover',
|
|
2202
|
-
success: 'successHover',
|
|
2203
|
-
warning: 'warningHover',
|
|
2204
|
-
};
|
|
2205
2246
|
/**
|
|
2206
2247
|
* Business logic for the Select component: id resolution, ref merging,
|
|
2207
2248
|
* controlled/uncontrolled value handling, open state, option grouping and
|
|
2208
2249
|
* focus restoration to the trigger when the menu closes.
|
|
2209
2250
|
*/
|
|
2210
|
-
const useSelect = ({ id, ref, value, defaultValue, onChange, options,
|
|
2251
|
+
const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled, }) => {
|
|
2211
2252
|
const generatedId = React.useId();
|
|
2212
2253
|
const fieldId = id ?? generatedId;
|
|
2254
|
+
const helperId = `${fieldId}-helper`;
|
|
2213
2255
|
const menuId = `${fieldId}-menu`;
|
|
2256
|
+
const labelId = `${fieldId}-label`;
|
|
2214
2257
|
const triggerRef = React.useRef(null);
|
|
2215
2258
|
const mergedRef = useMergedRefs(ref, triggerRef);
|
|
2216
2259
|
const [open, setOpen] = React.useState(false);
|
|
@@ -2254,6 +2297,8 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, status, di
|
|
|
2254
2297
|
const close = React.useCallback(() => setOpen(false), []);
|
|
2255
2298
|
return {
|
|
2256
2299
|
fieldId,
|
|
2300
|
+
labelId,
|
|
2301
|
+
helperId,
|
|
2257
2302
|
menuId,
|
|
2258
2303
|
triggerRef,
|
|
2259
2304
|
mergedRef,
|
|
@@ -2264,19 +2309,187 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, status, di
|
|
|
2264
2309
|
selectedOption,
|
|
2265
2310
|
groupedOptions,
|
|
2266
2311
|
handleSelect,
|
|
2267
|
-
helperColor: HELPER_COLOR_MAP[status],
|
|
2268
2312
|
};
|
|
2269
2313
|
};
|
|
2270
2314
|
|
|
2271
2315
|
const Select = ({ ref, value, defaultValue, onChange, options, label, helperText, placeholder, size = 'md', status = 'default', disabled, required, width, id, }) => {
|
|
2272
|
-
const { fieldId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect,
|
|
2273
|
-
return (jsxRuntime.jsxs(Stack, { flexDirection: 'column', gap: 'xs', style: { width: width ?? '100%' }, 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.jsx(SelectTrigger, { ref: mergedRef, id: fieldId, size: size, status: status, open: open, hasValue: selectedOption !== undefined, disabled: disabled,
|
|
2316
|
+
const { fieldId, labelId, helperId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect, } = useSelect({ id, ref, value, defaultValue, onChange, options, disabled });
|
|
2317
|
+
return (jsxRuntime.jsxs(Stack, { flexDirection: 'column', gap: 'xs', style: { width: width ?? '100%' }, children: [label !== undefined && (jsxRuntime.jsxs(Text, { variant: 'label', fontSize: 'sm', fontWeight: 'medium', color: 'textSecondary', htmlFor: fieldId, id: labelId, children: [label, required && (jsxRuntime.jsx(Text, { variant: 'span', color: 'errorMain', "aria-hidden": true, children: ' *' }))] })), jsxRuntime.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 }), jsxRuntime.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) => {
|
|
2274
2318
|
const items = groupOpts.map((opt) => (jsxRuntime.jsx(Menu.Item, { label: opt.label, icon: opt.icon, selected: opt.value === currentValue, disabled: opt.disabled, onClick: () => handleSelect(opt.value) }, opt.value)));
|
|
2275
2319
|
return groupKey !== undefined ? (jsxRuntime.jsx(Menu.Group, { label: groupKey, divider: groupIndex > 0, children: items }, groupKey)) : (jsxRuntime.jsx(Menu.Group, { divider: groupIndex > 0, children: items }, '__ungrouped'));
|
|
2276
|
-
}) }), helperText !== undefined && (jsxRuntime.jsx(
|
|
2320
|
+
}) }), helperText !== undefined && (jsxRuntime.jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
|
|
2277
2321
|
};
|
|
2278
2322
|
Select.displayName = 'Select';
|
|
2279
2323
|
|
|
2324
|
+
const CHECKBOX_ROOT_VARIANTS = theme.createVariants((theme) => ({
|
|
2325
|
+
base: {
|
|
2326
|
+
display: 'inline-flex',
|
|
2327
|
+
alignItems: 'center',
|
|
2328
|
+
gap: theme.spacing.sm,
|
|
2329
|
+
cursor: 'pointer',
|
|
2330
|
+
userSelect: 'none',
|
|
2331
|
+
},
|
|
2332
|
+
variants: {
|
|
2333
|
+
disabled: {
|
|
2334
|
+
true: {
|
|
2335
|
+
cursor: 'not-allowed',
|
|
2336
|
+
opacity: theme.opacity.high,
|
|
2337
|
+
},
|
|
2338
|
+
false: {},
|
|
2339
|
+
},
|
|
2340
|
+
},
|
|
2341
|
+
defaultVariants: {
|
|
2342
|
+
disabled: 'false',
|
|
2343
|
+
},
|
|
2344
|
+
}), { id: 'checkbox-root' });
|
|
2345
|
+
const CHECKBOX_INPUT_VARIANTS = theme.createVariants((theme) => {
|
|
2346
|
+
const c = theme.colors;
|
|
2347
|
+
return {
|
|
2348
|
+
base: {
|
|
2349
|
+
appearance: 'none',
|
|
2350
|
+
position: 'relative',
|
|
2351
|
+
margin: 0,
|
|
2352
|
+
borderWidth: '1px',
|
|
2353
|
+
borderStyle: 'solid',
|
|
2354
|
+
borderColor: c.borderStrong,
|
|
2355
|
+
borderRadius: theme.radius.sm,
|
|
2356
|
+
backgroundColor: c.surfacePaper,
|
|
2357
|
+
transition: `border-color ${theme.transition.fast}, background-color ${theme.transition.fast}`,
|
|
2358
|
+
cursor: 'pointer',
|
|
2359
|
+
'&:focus-visible': {
|
|
2360
|
+
outline: '2px solid transparent',
|
|
2361
|
+
boxShadow: `0 0 0 2px ${c.primarySubtleActive}`,
|
|
2362
|
+
},
|
|
2363
|
+
'&::after': {
|
|
2364
|
+
content: '""',
|
|
2365
|
+
position: 'absolute',
|
|
2366
|
+
left: '50%',
|
|
2367
|
+
top: '50%',
|
|
2368
|
+
transform: 'translate(-50%, -56%) rotate(45deg)',
|
|
2369
|
+
width: '0.25rem',
|
|
2370
|
+
height: '0.5rem',
|
|
2371
|
+
borderRight: `2px solid ${c.textInverse}`,
|
|
2372
|
+
borderBottom: `2px solid ${c.textInverse}`,
|
|
2373
|
+
opacity: 0,
|
|
2374
|
+
},
|
|
2375
|
+
'&:checked::after': {
|
|
2376
|
+
opacity: 1,
|
|
2377
|
+
},
|
|
2378
|
+
'&:indeterminate::after': {
|
|
2379
|
+
width: '0.5rem',
|
|
2380
|
+
height: '0',
|
|
2381
|
+
borderRight: '0',
|
|
2382
|
+
borderBottom: `2px solid ${c.textInverse}`,
|
|
2383
|
+
transform: 'translate(-50%, -50%)',
|
|
2384
|
+
opacity: 1,
|
|
2385
|
+
},
|
|
2386
|
+
},
|
|
2387
|
+
variants: {
|
|
2388
|
+
size: {
|
|
2389
|
+
sm: {
|
|
2390
|
+
width: '1rem',
|
|
2391
|
+
height: '1rem',
|
|
2392
|
+
},
|
|
2393
|
+
md: {
|
|
2394
|
+
width: '1.125rem',
|
|
2395
|
+
height: '1.125rem',
|
|
2396
|
+
},
|
|
2397
|
+
lg: {
|
|
2398
|
+
width: '1.25rem',
|
|
2399
|
+
height: '1.25rem',
|
|
2400
|
+
},
|
|
2401
|
+
},
|
|
2402
|
+
status: {
|
|
2403
|
+
default: {
|
|
2404
|
+
'&:checked, &:indeterminate': {
|
|
2405
|
+
borderColor: c.primaryMain,
|
|
2406
|
+
backgroundColor: c.primaryMain,
|
|
2407
|
+
},
|
|
2408
|
+
},
|
|
2409
|
+
error: {
|
|
2410
|
+
borderColor: c.errorMain,
|
|
2411
|
+
'&:checked, &:indeterminate': {
|
|
2412
|
+
borderColor: c.errorMain,
|
|
2413
|
+
backgroundColor: c.errorMain,
|
|
2414
|
+
},
|
|
2415
|
+
},
|
|
2416
|
+
success: {
|
|
2417
|
+
borderColor: c.successMain,
|
|
2418
|
+
'&:checked, &:indeterminate': {
|
|
2419
|
+
borderColor: c.successMain,
|
|
2420
|
+
backgroundColor: c.successMain,
|
|
2421
|
+
},
|
|
2422
|
+
},
|
|
2423
|
+
warning: {
|
|
2424
|
+
borderColor: c.warningMain,
|
|
2425
|
+
'&:checked, &:indeterminate': {
|
|
2426
|
+
borderColor: c.warningMain,
|
|
2427
|
+
backgroundColor: c.warningMain,
|
|
2428
|
+
},
|
|
2429
|
+
},
|
|
2430
|
+
},
|
|
2431
|
+
disabled: {
|
|
2432
|
+
true: {
|
|
2433
|
+
cursor: 'not-allowed',
|
|
2434
|
+
backgroundColor: c.disabledMain,
|
|
2435
|
+
borderColor: c.disabledMain,
|
|
2436
|
+
'&:checked, &:indeterminate': {
|
|
2437
|
+
backgroundColor: c.disabledText,
|
|
2438
|
+
borderColor: c.disabledText,
|
|
2439
|
+
},
|
|
2440
|
+
},
|
|
2441
|
+
false: {},
|
|
2442
|
+
},
|
|
2443
|
+
},
|
|
2444
|
+
defaultVariants: {
|
|
2445
|
+
size: 'md',
|
|
2446
|
+
status: 'default',
|
|
2447
|
+
disabled: 'false',
|
|
2448
|
+
},
|
|
2449
|
+
};
|
|
2450
|
+
}, { id: 'checkbox-input' });
|
|
2451
|
+
const CHECKBOX_STYLES = theme.createStyles((theme) => ({
|
|
2452
|
+
wrapper: {
|
|
2453
|
+
display: 'inline-flex',
|
|
2454
|
+
flexDirection: 'column',
|
|
2455
|
+
gap: theme.spacing.xs,
|
|
2456
|
+
},
|
|
2457
|
+
helper: {
|
|
2458
|
+
marginLeft: `calc(1.125rem + ${theme.spacing.sm})`,
|
|
2459
|
+
},
|
|
2460
|
+
}), { id: 'checkbox-extra' });
|
|
2461
|
+
|
|
2462
|
+
/** Handles id generation, ref merging and native indeterminate state sync. */
|
|
2463
|
+
const useCheckbox = ({ id, ref, indeterminate = false }) => {
|
|
2464
|
+
const generatedId = React.useId();
|
|
2465
|
+
const checkboxId = id ?? generatedId;
|
|
2466
|
+
const helperId = `${checkboxId}-helper`;
|
|
2467
|
+
const inputRef = React.useRef(null);
|
|
2468
|
+
const mergedRef = useMergedRefs(ref, inputRef);
|
|
2469
|
+
React.useEffect(() => {
|
|
2470
|
+
if (inputRef.current) {
|
|
2471
|
+
inputRef.current.indeterminate = indeterminate;
|
|
2472
|
+
}
|
|
2473
|
+
}, [indeterminate]);
|
|
2474
|
+
return {
|
|
2475
|
+
checkboxId,
|
|
2476
|
+
helperId,
|
|
2477
|
+
mergedRef,
|
|
2478
|
+
inputRef,
|
|
2479
|
+
};
|
|
2480
|
+
};
|
|
2481
|
+
|
|
2482
|
+
const Checkbox = ({ ref, label, helperText, size = 'md', status = 'default', indeterminate = false, error, id, disabled, required, ...rest }) => {
|
|
2483
|
+
const resolvedStatus = error ? 'error' : status;
|
|
2484
|
+
const { checkboxId, helperId, mergedRef } = useCheckbox({ id, ref, indeterminate });
|
|
2485
|
+
return (jsxRuntime.jsxs("div", { className: CHECKBOX_STYLES.wrapper, children: [jsxRuntime.jsxs("label", { htmlFor: checkboxId, className: CHECKBOX_ROOT_VARIANTS({ disabled: disabled ? 'true' : 'false' }), children: [jsxRuntime.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({
|
|
2486
|
+
size,
|
|
2487
|
+
status: resolvedStatus,
|
|
2488
|
+
disabled: disabled ? 'true' : 'false',
|
|
2489
|
+
}), ...rest }), label !== undefined && (jsxRuntime.jsx(Text, { variant: 'span', fontSize: size === 'lg' ? 'md' : 'sm', color: disabled ? 'textDisabled' : 'textSecondary', children: label }))] }), helperText !== undefined && (jsxRuntime.jsx(FormHelperText, { id: helperId, status: resolvedStatus, className: CHECKBOX_STYLES.helper, children: helperText }))] }));
|
|
2490
|
+
};
|
|
2491
|
+
Checkbox.displayName = 'Checkbox';
|
|
2492
|
+
|
|
2280
2493
|
const CARD_VARIANTS = theme.createVariants((theme) => ({
|
|
2281
2494
|
base: {
|
|
2282
2495
|
boxSizing: 'border-box',
|
|
@@ -2360,6 +2573,361 @@ const Grid = ({ display = 'grid', columns, rows, autoFlow, autoColumns, autoRows
|
|
|
2360
2573
|
};
|
|
2361
2574
|
Grid.displayName = 'Grid';
|
|
2362
2575
|
|
|
2576
|
+
const DrawerContext = React.createContext({
|
|
2577
|
+
isExpanded: true,
|
|
2578
|
+
});
|
|
2579
|
+
const useDrawerContext = () => React.useContext(DrawerContext);
|
|
2580
|
+
|
|
2581
|
+
const TRANSITION$2 = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
|
|
2582
|
+
const DRAWER_STYLES = theme.createStyles((theme) => ({
|
|
2583
|
+
root: ({ isExpanded }) => ({
|
|
2584
|
+
display: 'flex',
|
|
2585
|
+
flexDirection: 'column',
|
|
2586
|
+
width: isExpanded ? EXPANDED_DRAWER_WIDTH : COLLAPSED_DRAWER_WIDTH,
|
|
2587
|
+
transition: `width ${theme.transition.slow}`,
|
|
2588
|
+
overflow: 'hidden',
|
|
2589
|
+
backgroundColor: theme.colors.surfacePaper,
|
|
2590
|
+
borderRight: `1px solid ${theme.colors.borderMain}`,
|
|
2591
|
+
boxSizing: 'border-box',
|
|
2592
|
+
flexShrink: 0,
|
|
2593
|
+
}),
|
|
2594
|
+
/** Temporary variant: slides in from the left as a fixed portal overlay. */
|
|
2595
|
+
temporaryPanel: {
|
|
2596
|
+
position: 'fixed',
|
|
2597
|
+
top: 0,
|
|
2598
|
+
left: 0,
|
|
2599
|
+
bottom: 0,
|
|
2600
|
+
width: EXPANDED_DRAWER_WIDTH,
|
|
2601
|
+
zIndex: theme.zIndex.modal,
|
|
2602
|
+
display: 'flex',
|
|
2603
|
+
flexDirection: 'column',
|
|
2604
|
+
backgroundColor: theme.colors.surfacePaper,
|
|
2605
|
+
borderRight: `1px solid ${theme.colors.borderMain}`,
|
|
2606
|
+
boxSizing: 'border-box',
|
|
2607
|
+
overflowY: 'auto',
|
|
2608
|
+
overflowX: 'hidden',
|
|
2609
|
+
willChange: 'transform',
|
|
2610
|
+
transform: 'translateX(-100%)',
|
|
2611
|
+
transition: `transform ${TRANSITION$2}`,
|
|
2612
|
+
boxShadow: theme.shadows.xl,
|
|
2613
|
+
},
|
|
2614
|
+
temporaryPanelVisible: {
|
|
2615
|
+
transform: 'translateX(0)',
|
|
2616
|
+
},
|
|
2617
|
+
}));
|
|
2618
|
+
|
|
2619
|
+
/**
|
|
2620
|
+
* Responsive breakpoints (min-width, mobile-first).
|
|
2621
|
+
* Keep in sync with themeBreakpoints.
|
|
2622
|
+
*/
|
|
2623
|
+
const BREAKPOINTS = {
|
|
2624
|
+
sm: 640};
|
|
2625
|
+
/** Max-width media query strings (max = breakpoint - 1px). */
|
|
2626
|
+
const MEDIA_MAX = {
|
|
2627
|
+
sm: `max-width: ${BREAKPOINTS.sm - 1}px`};
|
|
2628
|
+
|
|
2629
|
+
const MOBILE_MQ = `(max-width: ${BREAKPOINTS.sm - 1}px)`;
|
|
2630
|
+
/**
|
|
2631
|
+
* Resolves the effective drawer variant based on the explicit `variant` prop
|
|
2632
|
+
* and the current viewport width.
|
|
2633
|
+
*
|
|
2634
|
+
* - If `variant` is explicitly provided (`'permanent'` or `'temporary'`), returns it as-is.
|
|
2635
|
+
* - Otherwise, auto-detects: `'temporary'` on mobile (< sm breakpoint), `'permanent'` on desktop.
|
|
2636
|
+
*
|
|
2637
|
+
* Reacts to viewport changes (window resize) so switching between mobile and desktop
|
|
2638
|
+
* automatically updates the variant without requiring a page reload.
|
|
2639
|
+
*/
|
|
2640
|
+
const useDrawerVariant = (variant) => {
|
|
2641
|
+
const [isMobile, setIsMobile] = React.useState(() => {
|
|
2642
|
+
if (typeof window === 'undefined') {
|
|
2643
|
+
return false;
|
|
2644
|
+
}
|
|
2645
|
+
return window.matchMedia(MOBILE_MQ).matches;
|
|
2646
|
+
});
|
|
2647
|
+
React.useEffect(() => {
|
|
2648
|
+
if (typeof window === 'undefined') {
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
const mq = window.matchMedia(MOBILE_MQ);
|
|
2652
|
+
const handler = (e) => setIsMobile(e.matches);
|
|
2653
|
+
mq.addEventListener('change', handler);
|
|
2654
|
+
return () => mq.removeEventListener('change', handler);
|
|
2655
|
+
}, []);
|
|
2656
|
+
if (variant !== undefined) {
|
|
2657
|
+
return variant;
|
|
2658
|
+
}
|
|
2659
|
+
return isMobile ? 'temporary' : 'permanent';
|
|
2660
|
+
};
|
|
2661
|
+
|
|
2662
|
+
const TRANSITION$1 = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
|
|
2663
|
+
const BACKDROP_STYLES = theme.createStyles((theme) => ({
|
|
2664
|
+
root: {
|
|
2665
|
+
position: 'fixed',
|
|
2666
|
+
inset: 0,
|
|
2667
|
+
zIndex: theme.zIndex.modal - 1,
|
|
2668
|
+
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
2669
|
+
transition: `background-color ${TRANSITION$1}`,
|
|
2670
|
+
},
|
|
2671
|
+
visible: {
|
|
2672
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
2673
|
+
},
|
|
2674
|
+
}), { id: 'backdrop' });
|
|
2675
|
+
|
|
2676
|
+
/**
|
|
2677
|
+
* Semi-transparent full-screen overlay used to visually block page content
|
|
2678
|
+
* while a modal element (dialog, temporary drawer…) is open.
|
|
2679
|
+
*
|
|
2680
|
+
* The `visible` prop drives the opacity transition: `false` = transparent,
|
|
2681
|
+
* `true` = `rgba(0,0,0,0.5)`. Mount/unmount is managed by the parent.
|
|
2682
|
+
*
|
|
2683
|
+
* @example
|
|
2684
|
+
* <Backdrop visible={isFadingIn} onClick={onClose} />
|
|
2685
|
+
*/
|
|
2686
|
+
const Backdrop = ({ visible, onClick }) => (jsxRuntime.jsx("div", { className: theme.cx(BACKDROP_STYLES.root, visible && BACKDROP_STYLES.visible), onClick: onClick, "aria-hidden": true }));
|
|
2687
|
+
Backdrop.displayName = 'Backdrop';
|
|
2688
|
+
|
|
2689
|
+
/**
|
|
2690
|
+
* Manages mount/unmount transitions with a two-phase approach (visible → fading-in).
|
|
2691
|
+
*
|
|
2692
|
+
* Opening sequence:
|
|
2693
|
+
* 1. `useLayoutEffect` sets `isVisible=true` synchronously before the browser paints
|
|
2694
|
+
* → element is in the DOM at its initial hidden state on the very first frame.
|
|
2695
|
+
* 2. Double `requestAnimationFrame` in `useEffect` waits for two rendered frames
|
|
2696
|
+
* before setting `isFadingIn=true`, guaranteeing the CSS transition always starts
|
|
2697
|
+
* from the painted hidden state (prevents flickering).
|
|
2698
|
+
*
|
|
2699
|
+
* Closing sequence:
|
|
2700
|
+
* `isFadingIn=false` → CSS transition plays → after `duration` ms → `isVisible=false`.
|
|
2701
|
+
*
|
|
2702
|
+
* @param isOpen - Whether the element should be shown.
|
|
2703
|
+
* @param duration - Transition duration in milliseconds (defaults to `DEFAULT_TRANSITION_DURATION_MS`).
|
|
2704
|
+
*/
|
|
2705
|
+
const useTransitionRender = (isOpen, duration = DEFAULT_TRANSITION_DURATION_MS) => {
|
|
2706
|
+
const [isVisible, setIsVisible] = React.useState(isOpen);
|
|
2707
|
+
const [isFadingIn, setIsFadingIn] = React.useState(isOpen);
|
|
2708
|
+
// Mount synchronously before paint so the element is in the DOM at opacity:0
|
|
2709
|
+
// on the very first frame — no extra render cycle between null and the initial state.
|
|
2710
|
+
React.useLayoutEffect(() => {
|
|
2711
|
+
if (isOpen) {
|
|
2712
|
+
setIsVisible(true);
|
|
2713
|
+
}
|
|
2714
|
+
}, [isOpen]);
|
|
2715
|
+
React.useEffect(() => {
|
|
2716
|
+
if (isOpen) {
|
|
2717
|
+
// Double RAF: frame 1 → element rendered; frame 2 → element painted at hidden
|
|
2718
|
+
// state → THEN trigger the CSS transition.
|
|
2719
|
+
let raf2;
|
|
2720
|
+
const raf1 = requestAnimationFrame(() => {
|
|
2721
|
+
raf2 = requestAnimationFrame(() => setIsFadingIn(true));
|
|
2722
|
+
});
|
|
2723
|
+
return () => {
|
|
2724
|
+
cancelAnimationFrame(raf1);
|
|
2725
|
+
cancelAnimationFrame(raf2);
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
else {
|
|
2729
|
+
setIsFadingIn(false);
|
|
2730
|
+
const timeout = setTimeout(() => setIsVisible(false), duration);
|
|
2731
|
+
return () => clearTimeout(timeout);
|
|
2732
|
+
}
|
|
2733
|
+
}, [isOpen, duration]);
|
|
2734
|
+
return { isVisible, isFadingIn };
|
|
2735
|
+
};
|
|
2736
|
+
|
|
2737
|
+
/**
|
|
2738
|
+
* Locks scrolling on `document.body` while `active` is true.
|
|
2739
|
+
*
|
|
2740
|
+
* Preserves the current scroll position and keeps the scrollbar gutter
|
|
2741
|
+
* (`overflow-y: scroll`) to avoid horizontal layout shift when the scrollbar
|
|
2742
|
+
* disappears. The original styles and scroll position are restored on cleanup.
|
|
2743
|
+
*
|
|
2744
|
+
* @example useBodyScrollLock(isDialogOpen)
|
|
2745
|
+
*/
|
|
2746
|
+
const useBodyScrollLock = (active) => {
|
|
2747
|
+
React.useEffect(() => {
|
|
2748
|
+
if (!active) {
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
const scrollY = window.scrollY;
|
|
2752
|
+
const body = document.body;
|
|
2753
|
+
body.style.position = 'fixed';
|
|
2754
|
+
body.style.top = `-${scrollY}px`;
|
|
2755
|
+
body.style.overflowY = 'scroll';
|
|
2756
|
+
body.style.width = '100%';
|
|
2757
|
+
return () => {
|
|
2758
|
+
body.style.position = '';
|
|
2759
|
+
body.style.top = '';
|
|
2760
|
+
body.style.overflowY = '';
|
|
2761
|
+
body.style.width = '';
|
|
2762
|
+
window.scrollTo(0, scrollY);
|
|
2763
|
+
};
|
|
2764
|
+
}, [active]);
|
|
2765
|
+
};
|
|
2766
|
+
|
|
2767
|
+
const DrawerHeader = ({ children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }) => {
|
|
2768
|
+
return (jsxRuntime.jsx(Box, { px: 'sm', py: 'xs', role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...rest, children: children }));
|
|
2769
|
+
};
|
|
2770
|
+
|
|
2771
|
+
const DrawerBody = ({ children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }) => {
|
|
2772
|
+
return (jsxRuntime.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 }));
|
|
2773
|
+
};
|
|
2774
|
+
|
|
2775
|
+
const DrawerFooter = ({ children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }) => {
|
|
2776
|
+
return (jsxRuntime.jsx(Box, { px: 'sm', py: 'xs', role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...rest, children: children }));
|
|
2777
|
+
};
|
|
2778
|
+
|
|
2779
|
+
const DRAWER_ITEM_STYLES = theme.createStyles((theme) => ({
|
|
2780
|
+
root: ({ selected, isExpanded }) => ({
|
|
2781
|
+
position: 'relative',
|
|
2782
|
+
display: 'flex',
|
|
2783
|
+
alignItems: 'center',
|
|
2784
|
+
width: isExpanded ? EXPANDED_DRAWER_ITEM_WIDTH : COLLAPSED_DRAWER_ITEM_WIDTH,
|
|
2785
|
+
gap: theme.spacing.sm,
|
|
2786
|
+
padding: `0 ${theme.spacing.smPlus}`,
|
|
2787
|
+
border: '1px solid transparent',
|
|
2788
|
+
borderRadius: theme.radius.md,
|
|
2789
|
+
fontFamily: 'inherit',
|
|
2790
|
+
fontSize: theme.fontSize.sm,
|
|
2791
|
+
userSelect: 'none',
|
|
2792
|
+
cursor: 'pointer',
|
|
2793
|
+
outline: 'none',
|
|
2794
|
+
textDecoration: 'none',
|
|
2795
|
+
boxSizing: 'border-box',
|
|
2796
|
+
transition: `width ${theme.transition.slow}, background-color ${theme.transition.fast}, color ${theme.transition.fast}`,
|
|
2797
|
+
whiteSpace: 'nowrap',
|
|
2798
|
+
overflow: 'hidden',
|
|
2799
|
+
height: DEFAULT_DRAWER_ITEM_SIZE,
|
|
2800
|
+
...(selected
|
|
2801
|
+
? {
|
|
2802
|
+
backgroundColor: theme.colors.primarySubtle,
|
|
2803
|
+
color: theme.colors.primaryMain,
|
|
2804
|
+
':hover': { backgroundColor: theme.colors.primarySubtleHover, color: theme.colors.primaryHover },
|
|
2805
|
+
':active': { backgroundColor: theme.colors.primarySubtleActive, color: theme.colors.primaryActive },
|
|
2806
|
+
}
|
|
2807
|
+
: {
|
|
2808
|
+
backgroundColor: 'transparent',
|
|
2809
|
+
color: theme.colors.defaultMain,
|
|
2810
|
+
':hover:not(:disabled)': { backgroundColor: theme.colors.defaultSubtleHover, color: theme.colors.defaultHover },
|
|
2811
|
+
':active:not(:disabled)': { backgroundColor: theme.colors.defaultSubtleActive, color: theme.colors.defaultActive },
|
|
2812
|
+
}),
|
|
2813
|
+
':focus-visible': { boxShadow: theme.shadows.focus },
|
|
2814
|
+
':disabled': { cursor: 'not-allowed', opacity: theme.opacity.high },
|
|
2815
|
+
}),
|
|
2816
|
+
iconWrap: {
|
|
2817
|
+
display: 'flex',
|
|
2818
|
+
alignItems: 'center',
|
|
2819
|
+
justifyContent: 'center',
|
|
2820
|
+
flexShrink: 0,
|
|
2821
|
+
},
|
|
2822
|
+
labelWrapper: ({ isFadingIn }) => ({
|
|
2823
|
+
display: 'flex',
|
|
2824
|
+
alignItems: 'center',
|
|
2825
|
+
flexDirection: 'row',
|
|
2826
|
+
gap: theme.spacing.sm,
|
|
2827
|
+
width: '100%',
|
|
2828
|
+
justifyContent: 'space-between',
|
|
2829
|
+
flex: 1,
|
|
2830
|
+
overflow: 'hidden',
|
|
2831
|
+
minWidth: 0,
|
|
2832
|
+
opacity: isFadingIn ? 1 : 0,
|
|
2833
|
+
transition: `opacity ${DEFAULT_TRANSITION_DURATION_MS}ms ease`,
|
|
2834
|
+
}),
|
|
2835
|
+
label: {
|
|
2836
|
+
flex: 1,
|
|
2837
|
+
overflow: 'hidden',
|
|
2838
|
+
textOverflow: 'ellipsis',
|
|
2839
|
+
fontWeight: theme.fontWeight.medium,
|
|
2840
|
+
},
|
|
2841
|
+
endContent: {
|
|
2842
|
+
display: 'flex',
|
|
2843
|
+
alignItems: 'center',
|
|
2844
|
+
flexShrink: 0,
|
|
2845
|
+
marginLeft: 'auto',
|
|
2846
|
+
},
|
|
2847
|
+
}));
|
|
2848
|
+
|
|
2849
|
+
/**
|
|
2850
|
+
* Navigation/action item for use inside Drawer.Body or Drawer.Footer.
|
|
2851
|
+
*
|
|
2852
|
+
* - In **expanded** mode: shows icon + label (fade-in) + optional end content.
|
|
2853
|
+
* - In **collapsed** mode: shows only the icon; the label fades out before unmounting
|
|
2854
|
+
* and appears as a right-side tooltip on hover.
|
|
2855
|
+
* - Renders as `<a>` when `href` is provided, otherwise as `<button>`.
|
|
2856
|
+
* - The `selected` prop applies a primary-color highlight.
|
|
2857
|
+
*/
|
|
2858
|
+
const DrawerItem = ({ startIcon, label, selected = false, endContent, href, onClick, disabled = false, ariaLabel, ariaLabelledBy, ariaDescribedBy, ariaControls, ariaExpanded, ariaHasPopup, ariaCurrent, }) => {
|
|
2859
|
+
const { isExpanded } = useDrawerContext();
|
|
2860
|
+
const { isVisible: isLabelVisible, isFadingIn: isLabelFadingIn } = useTransitionRender(isExpanded);
|
|
2861
|
+
const rootClassName = DRAWER_ITEM_STYLES.root({ selected, isExpanded });
|
|
2862
|
+
const computedAriaLabel = ariaLabel ?? (!isExpanded ? label : undefined);
|
|
2863
|
+
const computedAriaCurrent = ariaCurrent ?? (selected ? 'page' : undefined);
|
|
2864
|
+
const handleClick = (e) => {
|
|
2865
|
+
if (disabled) {
|
|
2866
|
+
e.preventDefault();
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
onClick?.();
|
|
2870
|
+
};
|
|
2871
|
+
const innerContent = (jsxRuntime.jsxs(Stack, { width: '100%', justifyContent: 'start', children: [jsxRuntime.jsx("span", { className: DRAWER_ITEM_STYLES.iconWrap, children: jsxRuntime.jsx(Icon, { icon: startIcon, size: 'md' }) }), isLabelVisible && (jsxRuntime.jsxs("span", { className: DRAWER_ITEM_STYLES.labelWrapper({ isFadingIn: isLabelFadingIn }), children: [jsxRuntime.jsx(Text, { variant: 'span', fontSize: 'sm', fontWeight: 'medium', className: DRAWER_ITEM_STYLES.label, textAlign: 'start', children: label }), endContent && (jsxRuntime.jsx("span", { className: DRAWER_ITEM_STYLES.endContent, children: endContent }))] }))] }));
|
|
2872
|
+
const item = href ? (jsxRuntime.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 })) : (jsxRuntime.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 }));
|
|
2873
|
+
return (jsxRuntime.jsx(Tooltip, { label: label, placement: 'right', inline: true, withArrow: true, disabled: isExpanded || disabled, children: item }));
|
|
2874
|
+
};
|
|
2875
|
+
DrawerItem.displayName = 'DrawerItem';
|
|
2876
|
+
|
|
2877
|
+
/**
|
|
2878
|
+
* Internal component: renders the temporary drawer as a fixed portal overlay
|
|
2879
|
+
* that slides in from the left with a backdrop, animated via `useTransitionRender`.
|
|
2880
|
+
*/
|
|
2881
|
+
const DrawerTemporaryPanel = ({ isExpanded, onClose, children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, }) => {
|
|
2882
|
+
const { isVisible, isFadingIn } = useTransitionRender(isExpanded);
|
|
2883
|
+
useBodyScrollLock(isExpanded);
|
|
2884
|
+
useKeyPress({ Escape: onClose }, { enabled: isExpanded });
|
|
2885
|
+
if (!isVisible) {
|
|
2886
|
+
return null;
|
|
2887
|
+
}
|
|
2888
|
+
return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Backdrop, { visible: isFadingIn, onClick: onClose }), jsxRuntime.jsx(DrawerContext.Provider, { value: { isExpanded: true }, children: jsxRuntime.jsx("nav", { className: theme.cx(DRAWER_STYLES.temporaryPanel, isFadingIn && DRAWER_STYLES.temporaryPanelVisible), role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, children: children }) })] }), document.body);
|
|
2889
|
+
};
|
|
2890
|
+
DrawerTemporaryPanel.displayName = 'DrawerTemporaryPanel';
|
|
2891
|
+
// ─── Main Drawer ─────────────────────────────────────────────────────────────
|
|
2892
|
+
/**
|
|
2893
|
+
* Collapsible side navigation drawer with controlled expanded/collapsed state.
|
|
2894
|
+
*
|
|
2895
|
+
* **Variants**
|
|
2896
|
+
* - `'permanent'` (default on desktop): inline drawer that pushes page content.
|
|
2897
|
+
* Toggles between `expanded` and `collapsed` states with animated width.
|
|
2898
|
+
* - `'temporary'` (default on mobile): portal overlay with a backdrop that slides
|
|
2899
|
+
* in from the left. `isExpanded` controls open/closed; `onClose` is called on
|
|
2900
|
+
* backdrop click or Escape.
|
|
2901
|
+
* - Omit `variant` to auto-detect based on viewport width.
|
|
2902
|
+
*
|
|
2903
|
+
* @example Permanent
|
|
2904
|
+
* <Drawer isExpanded={open} onExpandedChange={setOpen} height="100dvh">
|
|
2905
|
+
* <Drawer.Header>…</Drawer.Header>
|
|
2906
|
+
* <Drawer.Body>
|
|
2907
|
+
* <Drawer.Item startIcon={HomeIcon} label="Home" selected />
|
|
2908
|
+
* </Drawer.Body>
|
|
2909
|
+
* </Drawer>
|
|
2910
|
+
*
|
|
2911
|
+
* @example Temporary
|
|
2912
|
+
* <Drawer variant="temporary" isExpanded={open} onClose={() => setOpen(false)}>
|
|
2913
|
+
* …
|
|
2914
|
+
* </Drawer>
|
|
2915
|
+
*/
|
|
2916
|
+
const DrawerBase = ({ height = '100dvh', isExpanded, onExpandedChange, onClose, variant, children, role = 'navigation', ariaLabel = 'Navigation', ariaLabelledBy, ariaDescribedBy, }) => {
|
|
2917
|
+
const resolvedVariant = useDrawerVariant(variant);
|
|
2918
|
+
const handleClose = onClose ?? (() => onExpandedChange?.(false));
|
|
2919
|
+
if (resolvedVariant === 'temporary') {
|
|
2920
|
+
return (jsxRuntime.jsx(DrawerTemporaryPanel, { isExpanded: isExpanded, onClose: handleClose, height: height, role: role, ariaLabel: ariaLabel, ariaLabelledBy: ariaLabelledBy, ariaDescribedBy: ariaDescribedBy, children: children }));
|
|
2921
|
+
}
|
|
2922
|
+
return (jsxRuntime.jsx(DrawerContext.Provider, { value: { isExpanded }, children: jsxRuntime.jsx("nav", { className: DRAWER_STYLES.root({ isExpanded }), style: { height }, role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, children: children }) }));
|
|
2923
|
+
};
|
|
2924
|
+
DrawerBase.displayName = 'Drawer';
|
|
2925
|
+
const Drawer = DrawerBase;
|
|
2926
|
+
Drawer.Header = DrawerHeader;
|
|
2927
|
+
Drawer.Body = DrawerBody;
|
|
2928
|
+
Drawer.Footer = DrawerFooter;
|
|
2929
|
+
Drawer.Item = DrawerItem;
|
|
2930
|
+
|
|
2363
2931
|
const AlertContext = React.createContext({
|
|
2364
2932
|
variant: 'default',
|
|
2365
2933
|
accentColor: 'defaultActive',
|
|
@@ -2412,6 +2980,15 @@ const VARIANT_ARIA_LIVE = {
|
|
|
2412
2980
|
warning: 'polite',
|
|
2413
2981
|
info: 'polite',
|
|
2414
2982
|
};
|
|
2983
|
+
/** role="alert" implies aria-live="assertive" — reserve it for errors.
|
|
2984
|
+
* Other variants use role="status" (implies aria-live="polite"). */
|
|
2985
|
+
const VARIANT_ROLE = {
|
|
2986
|
+
default: 'status',
|
|
2987
|
+
success: 'status',
|
|
2988
|
+
error: 'alert',
|
|
2989
|
+
warning: 'status',
|
|
2990
|
+
info: 'status',
|
|
2991
|
+
};
|
|
2415
2992
|
/**
|
|
2416
2993
|
* Inline alert banner with 5 visual variants: default, success, error, warning, info.
|
|
2417
2994
|
* Use `Alert.Title` (icon + heading) and `Alert.Body` (message) as children.
|
|
@@ -2424,38 +3001,15 @@ const VARIANT_ARIA_LIVE = {
|
|
|
2424
3001
|
*/
|
|
2425
3002
|
const AlertBase = ({ variant = 'default', children, width = '100%', outline = false, shadow = 'none', }) => {
|
|
2426
3003
|
const { backgroundColor, borderColor, accentColor } = VARIANT_TOKENS[variant];
|
|
2427
|
-
return (jsxRuntime.jsx(AlertContext.Provider, { value: { variant, accentColor }, children: jsxRuntime.jsx(Stack, { role:
|
|
3004
|
+
return (jsxRuntime.jsx(AlertContext.Provider, { value: { variant, accentColor }, children: jsxRuntime.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 }) }));
|
|
2428
3005
|
};
|
|
2429
3006
|
AlertBase.displayName = 'Alert';
|
|
2430
3007
|
const Alert = AlertBase;
|
|
2431
3008
|
Alert.Title = AlertTitle;
|
|
2432
3009
|
Alert.Body = AlertBody;
|
|
2433
3010
|
|
|
2434
|
-
/**
|
|
2435
|
-
* Responsive breakpoints (min-width, mobile-first).
|
|
2436
|
-
* Keep in sync with themeBreakpoints.
|
|
2437
|
-
*/
|
|
2438
|
-
const BREAKPOINTS = {
|
|
2439
|
-
sm: 640};
|
|
2440
|
-
/** Max-width media query strings (max = breakpoint - 1px). */
|
|
2441
|
-
const MEDIA_MAX = {
|
|
2442
|
-
sm: `max-width: ${BREAKPOINTS.sm - 1}px`};
|
|
2443
|
-
|
|
2444
|
-
/** Default duration in milliseconds for mount/unmount transition animations. */
|
|
2445
|
-
const DEFAULT_TRANSITION_DURATION_MS = 250;
|
|
2446
|
-
|
|
2447
3011
|
const TRANSITION = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
|
|
2448
3012
|
const DIALOG_STYLES = theme.createStyles((theme) => ({
|
|
2449
|
-
backdrop: {
|
|
2450
|
-
position: 'fixed',
|
|
2451
|
-
inset: 0,
|
|
2452
|
-
zIndex: theme.zIndex.modal - 1,
|
|
2453
|
-
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
2454
|
-
transition: `background-color ${TRANSITION}`,
|
|
2455
|
-
},
|
|
2456
|
-
backdropVisible: {
|
|
2457
|
-
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
2458
|
-
},
|
|
2459
3013
|
panel: {
|
|
2460
3014
|
position: 'fixed',
|
|
2461
3015
|
inset: 0,
|
|
@@ -2513,84 +3067,6 @@ const DialogContext = React.createContext({
|
|
|
2513
3067
|
CloseIconComponent: null,
|
|
2514
3068
|
});
|
|
2515
3069
|
|
|
2516
|
-
/**
|
|
2517
|
-
* Manages mount/unmount transitions with a two-phase approach (visible → fading-in).
|
|
2518
|
-
*
|
|
2519
|
-
* Opening sequence:
|
|
2520
|
-
* 1. `useLayoutEffect` sets `isVisible=true` synchronously before the browser paints
|
|
2521
|
-
* → element is in the DOM at its initial hidden state on the very first frame.
|
|
2522
|
-
* 2. Double `requestAnimationFrame` in `useEffect` waits for two rendered frames
|
|
2523
|
-
* before setting `isFadingIn=true`, guaranteeing the CSS transition always starts
|
|
2524
|
-
* from the painted hidden state (prevents flickering).
|
|
2525
|
-
*
|
|
2526
|
-
* Closing sequence:
|
|
2527
|
-
* `isFadingIn=false` → CSS transition plays → after `duration` ms → `isVisible=false`.
|
|
2528
|
-
*
|
|
2529
|
-
* @param isOpen - Whether the element should be shown.
|
|
2530
|
-
* @param duration - Transition duration in milliseconds (defaults to `DEFAULT_TRANSITION_DURATION_MS`).
|
|
2531
|
-
*/
|
|
2532
|
-
const useTransitionRender = (isOpen, duration = DEFAULT_TRANSITION_DURATION_MS) => {
|
|
2533
|
-
const [isVisible, setIsVisible] = React.useState(isOpen);
|
|
2534
|
-
const [isFadingIn, setIsFadingIn] = React.useState(isOpen);
|
|
2535
|
-
// Mount synchronously before paint so the element is in the DOM at opacity:0
|
|
2536
|
-
// on the very first frame — no extra render cycle between null and the initial state.
|
|
2537
|
-
React.useLayoutEffect(() => {
|
|
2538
|
-
if (isOpen) {
|
|
2539
|
-
setIsVisible(true);
|
|
2540
|
-
}
|
|
2541
|
-
}, [isOpen]);
|
|
2542
|
-
React.useEffect(() => {
|
|
2543
|
-
if (isOpen) {
|
|
2544
|
-
// Double RAF: frame 1 → element rendered; frame 2 → element painted at hidden
|
|
2545
|
-
// state → THEN trigger the CSS transition.
|
|
2546
|
-
let raf2;
|
|
2547
|
-
const raf1 = requestAnimationFrame(() => {
|
|
2548
|
-
raf2 = requestAnimationFrame(() => setIsFadingIn(true));
|
|
2549
|
-
});
|
|
2550
|
-
return () => {
|
|
2551
|
-
cancelAnimationFrame(raf1);
|
|
2552
|
-
cancelAnimationFrame(raf2);
|
|
2553
|
-
};
|
|
2554
|
-
}
|
|
2555
|
-
else {
|
|
2556
|
-
setIsFadingIn(false);
|
|
2557
|
-
const timeout = setTimeout(() => setIsVisible(false), duration);
|
|
2558
|
-
return () => clearTimeout(timeout);
|
|
2559
|
-
}
|
|
2560
|
-
}, [isOpen, duration]);
|
|
2561
|
-
return { isVisible, isFadingIn };
|
|
2562
|
-
};
|
|
2563
|
-
|
|
2564
|
-
/**
|
|
2565
|
-
* Locks scrolling on `document.body` while `active` is true.
|
|
2566
|
-
*
|
|
2567
|
-
* Preserves the current scroll position and keeps the scrollbar gutter
|
|
2568
|
-
* (`overflow-y: scroll`) to avoid horizontal layout shift when the scrollbar
|
|
2569
|
-
* disappears. The original styles and scroll position are restored on cleanup.
|
|
2570
|
-
*
|
|
2571
|
-
* @example useBodyScrollLock(isDialogOpen)
|
|
2572
|
-
*/
|
|
2573
|
-
const useBodyScrollLock = (active) => {
|
|
2574
|
-
React.useEffect(() => {
|
|
2575
|
-
if (!active) {
|
|
2576
|
-
return;
|
|
2577
|
-
}
|
|
2578
|
-
const scrollY = window.scrollY;
|
|
2579
|
-
const body = document.body;
|
|
2580
|
-
body.style.position = 'fixed';
|
|
2581
|
-
body.style.top = `-${scrollY}px`;
|
|
2582
|
-
body.style.overflowY = 'scroll';
|
|
2583
|
-
body.style.width = '100%';
|
|
2584
|
-
return () => {
|
|
2585
|
-
body.style.position = '';
|
|
2586
|
-
body.style.top = '';
|
|
2587
|
-
body.style.overflowY = '';
|
|
2588
|
-
body.style.width = '';
|
|
2589
|
-
window.scrollTo(0, scrollY);
|
|
2590
|
-
};
|
|
2591
|
-
}, [active]);
|
|
2592
|
-
};
|
|
2593
|
-
|
|
2594
3070
|
const FOCUSABLE_SELECTOR = [
|
|
2595
3071
|
'a[href]',
|
|
2596
3072
|
'button:not([disabled])',
|
|
@@ -2692,7 +3168,7 @@ const useDialog = ({ open, onClose, closeOnBackdropClick, maxWidth, maxHeight, m
|
|
|
2692
3168
|
*/
|
|
2693
3169
|
const DialogHeader = ({ title, onClose }) => {
|
|
2694
3170
|
const { titleId, CloseIconComponent } = React.useContext(DialogContext);
|
|
2695
|
-
return (jsxRuntime.jsxs(Stack, { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 'md', paddingTop: 'md', paddingBottom: 'md', paddingLeft: 'lg', paddingRight: 'md', flexShrink: 0, children: [jsxRuntime.jsx(Text, { id: titleId, variant: 'span', fontSize: 'md', fontWeight: 'semibold', color: 'textPrimary', children: title }), jsxRuntime.jsx(IconButton, { icon: CloseIconComponent, ariaLabel: 'Close dialog', variant: 'text', color: 'neutral', size: 'sm', type: 'button', onClick: onClose })] }));
|
|
3171
|
+
return (jsxRuntime.jsxs(Stack, { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 'md', paddingTop: 'md', paddingBottom: 'md', paddingLeft: 'lg', paddingRight: 'md', flexShrink: 0, children: [jsxRuntime.jsx(Text, { id: titleId, variant: 'span', as: 'h2', fontSize: 'md', fontWeight: 'semibold', color: 'textPrimary', children: title }), jsxRuntime.jsx(IconButton, { icon: CloseIconComponent, ariaLabel: 'Close dialog', variant: 'text', color: 'neutral', size: 'sm', type: 'button', onClick: onClose })] }));
|
|
2696
3172
|
};
|
|
2697
3173
|
DialogHeader.displayName = 'Dialog.Header';
|
|
2698
3174
|
|
|
@@ -2726,7 +3202,7 @@ const DialogBase = ({ open, onClose, children, closeOnBackdropClick = false, ful
|
|
|
2726
3202
|
if (!isVisible) {
|
|
2727
3203
|
return null;
|
|
2728
3204
|
}
|
|
2729
|
-
return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(
|
|
3205
|
+
return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Backdrop, { visible: isFadingIn, onClick: handleBackdropClick }), jsxRuntime.jsx("div", { ref: panelRef, role: 'dialog', "aria-modal": true, "aria-labelledby": labelledBy, "aria-label": ariaLabel, tabIndex: -1, className: theme.cx(DIALOG_STYLES.panel, isFadingIn && DIALOG_STYLES.panelVisible, fullscreen && DIALOG_STYLES.panelFullscreen), style: cssVars, children: jsxRuntime.jsx(DialogContext.Provider, { value: { titleId, CloseIconComponent: CloseIcon }, children: children }) })] }), document.body);
|
|
2730
3206
|
};
|
|
2731
3207
|
DialogBase.displayName = 'Dialog';
|
|
2732
3208
|
const Dialog = DialogBase;
|
|
@@ -2937,13 +3413,19 @@ const themeShadows = {
|
|
|
2937
3413
|
|
|
2938
3414
|
/**
|
|
2939
3415
|
* Default spacing tokens
|
|
3416
|
+
*
|
|
3417
|
+
* ⚠️ Token keys MUST stay CSS-custom-property safe (letters, digits, hyphens only).
|
|
3418
|
+
* They are turned into CSS variables (e.g. `smPlus` → `--theme-spacing-sm-plus`) by
|
|
3419
|
+
* the theme proxy, so characters like `+` would produce invalid variable names and
|
|
3420
|
+
* silently break any style that uses them.
|
|
2940
3421
|
*/
|
|
2941
3422
|
const themeSpacing = {
|
|
2942
3423
|
none: '0',
|
|
2943
3424
|
'2xs': '0.125rem', // 2px
|
|
2944
3425
|
xs: '0.25rem', // 4px
|
|
2945
|
-
|
|
3426
|
+
xsPlus: '0.375rem', // 6px
|
|
2946
3427
|
sm: '0.5rem', // 8px
|
|
3428
|
+
smPlus: '0.75rem', // 12px
|
|
2947
3429
|
md: '1rem', // 16px
|
|
2948
3430
|
lg: '1.5rem', // 24px
|
|
2949
3431
|
xl: '2rem', // 32px
|
|
@@ -2957,9 +3439,9 @@ const themeSpacing = {
|
|
|
2957
3439
|
* Default transition tokens
|
|
2958
3440
|
*/
|
|
2959
3441
|
const themeTransition = {
|
|
2960
|
-
fast: '150ms ease-out',
|
|
2961
|
-
normal: '250ms ease-out',
|
|
2962
|
-
slow: '350ms ease-out',
|
|
3442
|
+
fast: '150ms ease-in-out',
|
|
3443
|
+
normal: '250ms ease-in-out',
|
|
3444
|
+
slow: '350ms ease-in-out',
|
|
2963
3445
|
};
|
|
2964
3446
|
|
|
2965
3447
|
/**
|
|
@@ -3115,11 +3597,14 @@ const darkTheme = theme.createTheme({
|
|
|
3115
3597
|
});
|
|
3116
3598
|
|
|
3117
3599
|
exports.Alert = Alert;
|
|
3600
|
+
exports.Backdrop = Backdrop;
|
|
3118
3601
|
exports.Badge = Badge;
|
|
3119
3602
|
exports.Box = Box;
|
|
3120
3603
|
exports.Button = Button;
|
|
3121
3604
|
exports.Card = Card;
|
|
3605
|
+
exports.Checkbox = Checkbox;
|
|
3122
3606
|
exports.Dialog = Dialog;
|
|
3607
|
+
exports.Drawer = Drawer;
|
|
3123
3608
|
exports.Form = Form;
|
|
3124
3609
|
exports.Grid = Grid;
|
|
3125
3610
|
exports.Icon = Icon;
|
|
@@ -3136,4 +3621,5 @@ exports.TextField = TextField;
|
|
|
3136
3621
|
exports.Tooltip = Tooltip;
|
|
3137
3622
|
exports.darkTheme = darkTheme;
|
|
3138
3623
|
exports.lightTheme = lightTheme;
|
|
3624
|
+
exports.useDrawerContext = useDrawerContext;
|
|
3139
3625
|
//# sourceMappingURL=index.js.map
|