@aurora-ds/components 1.1.5 → 1.1.7
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 +662 -209
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.js +660 -211
- package/dist/esm/index.js.map +1 -1
- package/dist/index.d.ts +169 -4
- 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 }) => ({
|
|
@@ -559,7 +577,7 @@ const ICON_SIZE$2 = {
|
|
|
559
577
|
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 }) => {
|
|
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 } : {}),
|
|
@@ -570,21 +588,15 @@ const Button = ({ ref, variant = 'contained', color = 'primary', size = 'md', wi
|
|
|
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';
|
|
@@ -835,7 +851,7 @@ const BADGE_VARIANTS = createVariants((theme) => ({
|
|
|
835
851
|
size: {
|
|
836
852
|
sm: {
|
|
837
853
|
height: '1.25rem',
|
|
838
|
-
padding: `0.125rem ${theme.spacing
|
|
854
|
+
padding: `0.125rem ${theme.spacing.xsPlus}`,
|
|
839
855
|
fontSize: theme.fontSize['2xs'],
|
|
840
856
|
},
|
|
841
857
|
md: {
|
|
@@ -1236,6 +1252,12 @@ const SKELETON_VARIANTS = createVariants((theme) => ({
|
|
|
1236
1252
|
const Skeleton = ({ ref, variant = 'rectangular', animation = 'shimmer', width, height, className, style, ...rest }) => (jsx("span", { ref: ref, className: SKELETON_VARIANTS({ variant, animation: animation === false ? 'none' : animation }, className), style: { width, height, ...style }, "aria-hidden": true, ...rest }));
|
|
1237
1253
|
Skeleton.displayName = 'Skeleton';
|
|
1238
1254
|
|
|
1255
|
+
const FORM_STYLES = createStyles(() => ({
|
|
1256
|
+
root: {
|
|
1257
|
+
display: 'contents',
|
|
1258
|
+
},
|
|
1259
|
+
}));
|
|
1260
|
+
|
|
1239
1261
|
/**
|
|
1240
1262
|
* Thin wrapper around `<form>`. Prevents the default browser submit and
|
|
1241
1263
|
* delegates to the `onSubmit` callback.
|
|
@@ -1251,7 +1273,7 @@ const Form = ({ children, onSubmit, 'aria-label': ariaLabel, 'aria-labelledby':
|
|
|
1251
1273
|
event.preventDefault();
|
|
1252
1274
|
onSubmit(event);
|
|
1253
1275
|
};
|
|
1254
|
-
return (jsx("form", { onSubmit: handleSubmit, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, noValidate: true, children: children }));
|
|
1276
|
+
return (jsx("form", { onSubmit: handleSubmit, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: FORM_STYLES.root, noValidate: true, children: children }));
|
|
1255
1277
|
};
|
|
1256
1278
|
Form.displayName = 'Form';
|
|
1257
1279
|
|
|
@@ -1559,6 +1581,18 @@ Box.displayName = 'Box';
|
|
|
1559
1581
|
const Stack = ({ flexDirection = 'row', display = 'flex', gap = 'sm', ...rest }) => (jsx(Box, { display: display, flexDirection: flexDirection, gap: gap, ...rest }));
|
|
1560
1582
|
Stack.displayName = 'Stack';
|
|
1561
1583
|
|
|
1584
|
+
const HELPER_COLOR_MAP = {
|
|
1585
|
+
default: 'textSecondary',
|
|
1586
|
+
error: 'errorHover',
|
|
1587
|
+
success: 'successHover',
|
|
1588
|
+
warning: 'warningHover',
|
|
1589
|
+
};
|
|
1590
|
+
const FormHelperText = ({ id, status = 'default', ariaLive, className, children, }) => {
|
|
1591
|
+
const resolvedAriaLive = ariaLive ?? (status === 'error' ? 'assertive' : 'polite');
|
|
1592
|
+
return (jsx(Text, { id: id, variant: 'span', fontSize: 'xs', color: HELPER_COLOR_MAP[status], "aria-live": resolvedAriaLive, className: className, children: children }));
|
|
1593
|
+
};
|
|
1594
|
+
FormHelperText.displayName = 'FormHelperText';
|
|
1595
|
+
|
|
1562
1596
|
const TEXTFIELD_WRAPPER_VARIANTS = createVariants((theme) => {
|
|
1563
1597
|
const c = theme.colors;
|
|
1564
1598
|
return {
|
|
@@ -1681,18 +1715,11 @@ const ICON_BUTTON_SIZE_MAP = {
|
|
|
1681
1715
|
md: 'sm',
|
|
1682
1716
|
lg: 'md',
|
|
1683
1717
|
};
|
|
1684
|
-
/** Maps status to a theme color token used for the helper text. */
|
|
1685
|
-
const HELPER_COLOR_MAP$1 = {
|
|
1686
|
-
default: 'textSecondary',
|
|
1687
|
-
error: 'errorHover',
|
|
1688
|
-
success: 'successHover',
|
|
1689
|
-
warning: 'warningHover',
|
|
1690
|
-
};
|
|
1691
1718
|
/**
|
|
1692
1719
|
* Business logic for the TextField component: id resolution, ref merging,
|
|
1693
1720
|
* password visibility toggling and size/status derived tokens.
|
|
1694
1721
|
*/
|
|
1695
|
-
const useTextField = ({ id, ref, type, size,
|
|
1722
|
+
const useTextField = ({ id, ref, type, size, endAction, }) => {
|
|
1696
1723
|
const generatedId = useId();
|
|
1697
1724
|
const fieldId = id ?? generatedId;
|
|
1698
1725
|
const helperId = `${fieldId}-helper`;
|
|
@@ -1714,7 +1741,6 @@ const useTextField = ({ id, ref, type, size, status, endAction, }) => {
|
|
|
1714
1741
|
resolvedType,
|
|
1715
1742
|
iconSize: ICON_SIZE_MAP$1[size],
|
|
1716
1743
|
iconButtonSize: ICON_BUTTON_SIZE_MAP[size],
|
|
1717
|
-
helperColor: HELPER_COLOR_MAP$1[status],
|
|
1718
1744
|
hasEndSection: endAction !== undefined || isPassword,
|
|
1719
1745
|
focusInput,
|
|
1720
1746
|
};
|
|
@@ -1732,8 +1758,8 @@ const useTextField = ({ id, ref, type, size, status, endAction, }) => {
|
|
|
1732
1758
|
* @example <TextField label="Search" startIcon={SearchIcon} endAction={<ClearButton />} />
|
|
1733
1759
|
*/
|
|
1734
1760
|
const TextField = ({ ref, label, helperText, size = 'md', status = 'default', startIcon: StartIcon, endAction, type, id, disabled, required, ...rest }) => {
|
|
1735
|
-
const { fieldId, helperId, mergedRef, isPassword, showPassword, togglePassword, resolvedType, iconSize, iconButtonSize,
|
|
1736
|
-
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', tabIndex: -1, onClick: togglePassword }))] }))] }), helperText !== undefined && (jsx(
|
|
1761
|
+
const { fieldId, helperId, mergedRef, isPassword, showPassword, togglePassword, resolvedType, iconSize, iconButtonSize, hasEndSection, focusInput, } = useTextField({ id, ref, type, size, endAction });
|
|
1762
|
+
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', tabIndex: -1, onClick: togglePassword }))] }))] }), helperText !== undefined && (jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
|
|
1737
1763
|
};
|
|
1738
1764
|
TextField.displayName = 'TextField';
|
|
1739
1765
|
|
|
@@ -2169,21 +2195,15 @@ const SelectTrigger = ({ ref, size = 'md', status = 'default', open, hasValue, s
|
|
|
2169
2195
|
};
|
|
2170
2196
|
SelectTrigger.displayName = 'SelectTrigger';
|
|
2171
2197
|
|
|
2172
|
-
/** Maps status to a theme color token used for the helper text. */
|
|
2173
|
-
const HELPER_COLOR_MAP = {
|
|
2174
|
-
default: 'textSecondary',
|
|
2175
|
-
error: 'errorHover',
|
|
2176
|
-
success: 'successHover',
|
|
2177
|
-
warning: 'warningHover',
|
|
2178
|
-
};
|
|
2179
2198
|
/**
|
|
2180
2199
|
* Business logic for the Select component: id resolution, ref merging,
|
|
2181
2200
|
* controlled/uncontrolled value handling, open state, option grouping and
|
|
2182
2201
|
* focus restoration to the trigger when the menu closes.
|
|
2183
2202
|
*/
|
|
2184
|
-
const useSelect = ({ id, ref, value, defaultValue, onChange, options,
|
|
2203
|
+
const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled, }) => {
|
|
2185
2204
|
const generatedId = useId();
|
|
2186
2205
|
const fieldId = id ?? generatedId;
|
|
2206
|
+
const helperId = `${fieldId}-helper`;
|
|
2187
2207
|
const menuId = `${fieldId}-menu`;
|
|
2188
2208
|
const triggerRef = useRef(null);
|
|
2189
2209
|
const mergedRef = useMergedRefs(ref, triggerRef);
|
|
@@ -2228,6 +2248,7 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, status, di
|
|
|
2228
2248
|
const close = useCallback(() => setOpen(false), []);
|
|
2229
2249
|
return {
|
|
2230
2250
|
fieldId,
|
|
2251
|
+
helperId,
|
|
2231
2252
|
menuId,
|
|
2232
2253
|
triggerRef,
|
|
2233
2254
|
mergedRef,
|
|
@@ -2238,19 +2259,187 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, status, di
|
|
|
2238
2259
|
selectedOption,
|
|
2239
2260
|
groupedOptions,
|
|
2240
2261
|
handleSelect,
|
|
2241
|
-
helperColor: HELPER_COLOR_MAP[status],
|
|
2242
2262
|
};
|
|
2243
2263
|
};
|
|
2244
2264
|
|
|
2245
2265
|
const Select = ({ ref, value, defaultValue, onChange, options, label, helperText, placeholder, size = 'md', status = 'default', disabled, required, width, id, }) => {
|
|
2246
|
-
const { fieldId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect,
|
|
2247
|
-
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, required: required, "aria-haspopup": 'listbox', "aria-expanded": open, "aria-controls": menuId, "aria-invalid": status === 'error' || undefined, onClick: toggle, children: selectedOption !== undefined ? selectedOption.label : placeholder }), jsx(Menu, { open: open, onClose: close, anchorEl: triggerRef.current, id: menuId, children: Array.from(groupedOptions.entries()).map(([groupKey, groupOpts], groupIndex) => {
|
|
2266
|
+
const { fieldId, helperId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect, } = useSelect({ id, ref, value, defaultValue, onChange, options, disabled });
|
|
2267
|
+
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, required: required, "aria-haspopup": 'listbox', "aria-expanded": open, "aria-controls": menuId, "aria-invalid": status === 'error' || 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, children: Array.from(groupedOptions.entries()).map(([groupKey, groupOpts], groupIndex) => {
|
|
2248
2268
|
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)));
|
|
2249
2269
|
return groupKey !== undefined ? (jsx(Menu.Group, { label: groupKey, divider: groupIndex > 0, children: items }, groupKey)) : (jsx(Menu.Group, { divider: groupIndex > 0, children: items }, '__ungrouped'));
|
|
2250
|
-
}) }), helperText !== undefined && (jsx(
|
|
2270
|
+
}) }), helperText !== undefined && (jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
|
|
2251
2271
|
};
|
|
2252
2272
|
Select.displayName = 'Select';
|
|
2253
2273
|
|
|
2274
|
+
const CHECKBOX_ROOT_VARIANTS = createVariants((theme) => ({
|
|
2275
|
+
base: {
|
|
2276
|
+
display: 'inline-flex',
|
|
2277
|
+
alignItems: 'center',
|
|
2278
|
+
gap: theme.spacing.sm,
|
|
2279
|
+
cursor: 'pointer',
|
|
2280
|
+
userSelect: 'none',
|
|
2281
|
+
},
|
|
2282
|
+
variants: {
|
|
2283
|
+
disabled: {
|
|
2284
|
+
true: {
|
|
2285
|
+
cursor: 'not-allowed',
|
|
2286
|
+
opacity: theme.opacity.high,
|
|
2287
|
+
},
|
|
2288
|
+
false: {},
|
|
2289
|
+
},
|
|
2290
|
+
},
|
|
2291
|
+
defaultVariants: {
|
|
2292
|
+
disabled: 'false',
|
|
2293
|
+
},
|
|
2294
|
+
}), { id: 'checkbox-root' });
|
|
2295
|
+
const CHECKBOX_INPUT_VARIANTS = createVariants((theme) => {
|
|
2296
|
+
const c = theme.colors;
|
|
2297
|
+
return {
|
|
2298
|
+
base: {
|
|
2299
|
+
appearance: 'none',
|
|
2300
|
+
position: 'relative',
|
|
2301
|
+
margin: 0,
|
|
2302
|
+
borderWidth: '1px',
|
|
2303
|
+
borderStyle: 'solid',
|
|
2304
|
+
borderColor: c.borderStrong,
|
|
2305
|
+
borderRadius: theme.radius.sm,
|
|
2306
|
+
backgroundColor: c.surfacePaper,
|
|
2307
|
+
transition: `border-color ${theme.transition.fast}, background-color ${theme.transition.fast}`,
|
|
2308
|
+
cursor: 'pointer',
|
|
2309
|
+
'&:focus-visible': {
|
|
2310
|
+
outline: '2px solid transparent',
|
|
2311
|
+
boxShadow: `0 0 0 2px ${c.primarySubtleActive}`,
|
|
2312
|
+
},
|
|
2313
|
+
'&::after': {
|
|
2314
|
+
content: '""',
|
|
2315
|
+
position: 'absolute',
|
|
2316
|
+
left: '50%',
|
|
2317
|
+
top: '50%',
|
|
2318
|
+
transform: 'translate(-50%, -56%) rotate(45deg)',
|
|
2319
|
+
width: '0.25rem',
|
|
2320
|
+
height: '0.5rem',
|
|
2321
|
+
borderRight: `2px solid ${c.textInverse}`,
|
|
2322
|
+
borderBottom: `2px solid ${c.textInverse}`,
|
|
2323
|
+
opacity: 0,
|
|
2324
|
+
},
|
|
2325
|
+
'&:checked::after': {
|
|
2326
|
+
opacity: 1,
|
|
2327
|
+
},
|
|
2328
|
+
'&:indeterminate::after': {
|
|
2329
|
+
width: '0.5rem',
|
|
2330
|
+
height: '0',
|
|
2331
|
+
borderRight: '0',
|
|
2332
|
+
borderBottom: `2px solid ${c.textInverse}`,
|
|
2333
|
+
transform: 'translate(-50%, -50%)',
|
|
2334
|
+
opacity: 1,
|
|
2335
|
+
},
|
|
2336
|
+
},
|
|
2337
|
+
variants: {
|
|
2338
|
+
size: {
|
|
2339
|
+
sm: {
|
|
2340
|
+
width: '1rem',
|
|
2341
|
+
height: '1rem',
|
|
2342
|
+
},
|
|
2343
|
+
md: {
|
|
2344
|
+
width: '1.125rem',
|
|
2345
|
+
height: '1.125rem',
|
|
2346
|
+
},
|
|
2347
|
+
lg: {
|
|
2348
|
+
width: '1.25rem',
|
|
2349
|
+
height: '1.25rem',
|
|
2350
|
+
},
|
|
2351
|
+
},
|
|
2352
|
+
status: {
|
|
2353
|
+
default: {
|
|
2354
|
+
'&:checked, &:indeterminate': {
|
|
2355
|
+
borderColor: c.primaryMain,
|
|
2356
|
+
backgroundColor: c.primaryMain,
|
|
2357
|
+
},
|
|
2358
|
+
},
|
|
2359
|
+
error: {
|
|
2360
|
+
borderColor: c.errorMain,
|
|
2361
|
+
'&:checked, &:indeterminate': {
|
|
2362
|
+
borderColor: c.errorMain,
|
|
2363
|
+
backgroundColor: c.errorMain,
|
|
2364
|
+
},
|
|
2365
|
+
},
|
|
2366
|
+
success: {
|
|
2367
|
+
borderColor: c.successMain,
|
|
2368
|
+
'&:checked, &:indeterminate': {
|
|
2369
|
+
borderColor: c.successMain,
|
|
2370
|
+
backgroundColor: c.successMain,
|
|
2371
|
+
},
|
|
2372
|
+
},
|
|
2373
|
+
warning: {
|
|
2374
|
+
borderColor: c.warningMain,
|
|
2375
|
+
'&:checked, &:indeterminate': {
|
|
2376
|
+
borderColor: c.warningMain,
|
|
2377
|
+
backgroundColor: c.warningMain,
|
|
2378
|
+
},
|
|
2379
|
+
},
|
|
2380
|
+
},
|
|
2381
|
+
disabled: {
|
|
2382
|
+
true: {
|
|
2383
|
+
cursor: 'not-allowed',
|
|
2384
|
+
backgroundColor: c.disabledMain,
|
|
2385
|
+
borderColor: c.disabledMain,
|
|
2386
|
+
'&:checked, &:indeterminate': {
|
|
2387
|
+
backgroundColor: c.disabledText,
|
|
2388
|
+
borderColor: c.disabledText,
|
|
2389
|
+
},
|
|
2390
|
+
},
|
|
2391
|
+
false: {},
|
|
2392
|
+
},
|
|
2393
|
+
},
|
|
2394
|
+
defaultVariants: {
|
|
2395
|
+
size: 'md',
|
|
2396
|
+
status: 'default',
|
|
2397
|
+
disabled: 'false',
|
|
2398
|
+
},
|
|
2399
|
+
};
|
|
2400
|
+
}, { id: 'checkbox-input' });
|
|
2401
|
+
const CHECKBOX_STYLES = createStyles((theme) => ({
|
|
2402
|
+
wrapper: {
|
|
2403
|
+
display: 'inline-flex',
|
|
2404
|
+
flexDirection: 'column',
|
|
2405
|
+
gap: theme.spacing.xs,
|
|
2406
|
+
},
|
|
2407
|
+
helper: {
|
|
2408
|
+
marginLeft: `calc(1.125rem + ${theme.spacing.sm})`,
|
|
2409
|
+
},
|
|
2410
|
+
}), { id: 'checkbox-extra' });
|
|
2411
|
+
|
|
2412
|
+
/** Handles id generation, ref merging and native indeterminate state sync. */
|
|
2413
|
+
const useCheckbox = ({ id, ref, indeterminate = false }) => {
|
|
2414
|
+
const generatedId = useId();
|
|
2415
|
+
const checkboxId = id ?? generatedId;
|
|
2416
|
+
const helperId = `${checkboxId}-helper`;
|
|
2417
|
+
const inputRef = useRef(null);
|
|
2418
|
+
const mergedRef = useMergedRefs(ref, inputRef);
|
|
2419
|
+
useEffect(() => {
|
|
2420
|
+
if (inputRef.current) {
|
|
2421
|
+
inputRef.current.indeterminate = indeterminate;
|
|
2422
|
+
}
|
|
2423
|
+
}, [indeterminate]);
|
|
2424
|
+
return {
|
|
2425
|
+
checkboxId,
|
|
2426
|
+
helperId,
|
|
2427
|
+
mergedRef,
|
|
2428
|
+
inputRef,
|
|
2429
|
+
};
|
|
2430
|
+
};
|
|
2431
|
+
|
|
2432
|
+
const Checkbox = ({ ref, label, helperText, size = 'md', status = 'default', indeterminate = false, error, id, disabled, required, ...rest }) => {
|
|
2433
|
+
const resolvedStatus = error ? 'error' : status;
|
|
2434
|
+
const { checkboxId, helperId, mergedRef } = useCheckbox({ id, ref, indeterminate });
|
|
2435
|
+
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, className: CHECKBOX_INPUT_VARIANTS({
|
|
2436
|
+
size,
|
|
2437
|
+
status: resolvedStatus,
|
|
2438
|
+
disabled: disabled ? 'true' : 'false',
|
|
2439
|
+
}), ...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 }))] }));
|
|
2440
|
+
};
|
|
2441
|
+
Checkbox.displayName = 'Checkbox';
|
|
2442
|
+
|
|
2254
2443
|
const CARD_VARIANTS = createVariants((theme) => ({
|
|
2255
2444
|
base: {
|
|
2256
2445
|
boxSizing: 'border-box',
|
|
@@ -2334,6 +2523,361 @@ const Grid = ({ display = 'grid', columns, rows, autoFlow, autoColumns, autoRows
|
|
|
2334
2523
|
};
|
|
2335
2524
|
Grid.displayName = 'Grid';
|
|
2336
2525
|
|
|
2526
|
+
const DrawerContext = createContext({
|
|
2527
|
+
isExpanded: true,
|
|
2528
|
+
});
|
|
2529
|
+
const useDrawerContext = () => useContext(DrawerContext);
|
|
2530
|
+
|
|
2531
|
+
const TRANSITION$2 = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
|
|
2532
|
+
const DRAWER_STYLES = createStyles((theme) => ({
|
|
2533
|
+
root: ({ isExpanded }) => ({
|
|
2534
|
+
display: 'flex',
|
|
2535
|
+
flexDirection: 'column',
|
|
2536
|
+
width: isExpanded ? EXPANDED_DRAWER_WIDTH : COLLAPSED_DRAWER_WIDTH,
|
|
2537
|
+
transition: `width ${theme.transition.slow}`,
|
|
2538
|
+
overflow: 'hidden',
|
|
2539
|
+
backgroundColor: theme.colors.surfacePaper,
|
|
2540
|
+
borderRight: `1px solid ${theme.colors.borderMain}`,
|
|
2541
|
+
boxSizing: 'border-box',
|
|
2542
|
+
flexShrink: 0,
|
|
2543
|
+
}),
|
|
2544
|
+
/** Temporary variant: slides in from the left as a fixed portal overlay. */
|
|
2545
|
+
temporaryPanel: {
|
|
2546
|
+
position: 'fixed',
|
|
2547
|
+
top: 0,
|
|
2548
|
+
left: 0,
|
|
2549
|
+
bottom: 0,
|
|
2550
|
+
width: EXPANDED_DRAWER_WIDTH,
|
|
2551
|
+
zIndex: theme.zIndex.modal,
|
|
2552
|
+
display: 'flex',
|
|
2553
|
+
flexDirection: 'column',
|
|
2554
|
+
backgroundColor: theme.colors.surfacePaper,
|
|
2555
|
+
borderRight: `1px solid ${theme.colors.borderMain}`,
|
|
2556
|
+
boxSizing: 'border-box',
|
|
2557
|
+
overflowY: 'auto',
|
|
2558
|
+
overflowX: 'hidden',
|
|
2559
|
+
willChange: 'transform',
|
|
2560
|
+
transform: 'translateX(-100%)',
|
|
2561
|
+
transition: `transform ${TRANSITION$2}`,
|
|
2562
|
+
boxShadow: theme.shadows.xl,
|
|
2563
|
+
},
|
|
2564
|
+
temporaryPanelVisible: {
|
|
2565
|
+
transform: 'translateX(0)',
|
|
2566
|
+
},
|
|
2567
|
+
}));
|
|
2568
|
+
|
|
2569
|
+
/**
|
|
2570
|
+
* Responsive breakpoints (min-width, mobile-first).
|
|
2571
|
+
* Keep in sync with themeBreakpoints.
|
|
2572
|
+
*/
|
|
2573
|
+
const BREAKPOINTS = {
|
|
2574
|
+
sm: 640};
|
|
2575
|
+
/** Max-width media query strings (max = breakpoint - 1px). */
|
|
2576
|
+
const MEDIA_MAX = {
|
|
2577
|
+
sm: `max-width: ${BREAKPOINTS.sm - 1}px`};
|
|
2578
|
+
|
|
2579
|
+
const MOBILE_MQ = `(max-width: ${BREAKPOINTS.sm - 1}px)`;
|
|
2580
|
+
/**
|
|
2581
|
+
* Resolves the effective drawer variant based on the explicit `variant` prop
|
|
2582
|
+
* and the current viewport width.
|
|
2583
|
+
*
|
|
2584
|
+
* - If `variant` is explicitly provided (`'permanent'` or `'temporary'`), returns it as-is.
|
|
2585
|
+
* - Otherwise, auto-detects: `'temporary'` on mobile (< sm breakpoint), `'permanent'` on desktop.
|
|
2586
|
+
*
|
|
2587
|
+
* Reacts to viewport changes (window resize) so switching between mobile and desktop
|
|
2588
|
+
* automatically updates the variant without requiring a page reload.
|
|
2589
|
+
*/
|
|
2590
|
+
const useDrawerVariant = (variant) => {
|
|
2591
|
+
const [isMobile, setIsMobile] = useState(() => {
|
|
2592
|
+
if (typeof window === 'undefined') {
|
|
2593
|
+
return false;
|
|
2594
|
+
}
|
|
2595
|
+
return window.matchMedia(MOBILE_MQ).matches;
|
|
2596
|
+
});
|
|
2597
|
+
useEffect(() => {
|
|
2598
|
+
if (typeof window === 'undefined') {
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
const mq = window.matchMedia(MOBILE_MQ);
|
|
2602
|
+
const handler = (e) => setIsMobile(e.matches);
|
|
2603
|
+
mq.addEventListener('change', handler);
|
|
2604
|
+
return () => mq.removeEventListener('change', handler);
|
|
2605
|
+
}, []);
|
|
2606
|
+
if (variant !== undefined) {
|
|
2607
|
+
return variant;
|
|
2608
|
+
}
|
|
2609
|
+
return isMobile ? 'temporary' : 'permanent';
|
|
2610
|
+
};
|
|
2611
|
+
|
|
2612
|
+
const TRANSITION$1 = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
|
|
2613
|
+
const BACKDROP_STYLES = createStyles((theme) => ({
|
|
2614
|
+
root: {
|
|
2615
|
+
position: 'fixed',
|
|
2616
|
+
inset: 0,
|
|
2617
|
+
zIndex: theme.zIndex.modal - 1,
|
|
2618
|
+
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
2619
|
+
transition: `background-color ${TRANSITION$1}`,
|
|
2620
|
+
},
|
|
2621
|
+
visible: {
|
|
2622
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
2623
|
+
},
|
|
2624
|
+
}), { id: 'backdrop' });
|
|
2625
|
+
|
|
2626
|
+
/**
|
|
2627
|
+
* Semi-transparent full-screen overlay used to visually block page content
|
|
2628
|
+
* while a modal element (dialog, temporary drawer…) is open.
|
|
2629
|
+
*
|
|
2630
|
+
* The `visible` prop drives the opacity transition: `false` = transparent,
|
|
2631
|
+
* `true` = `rgba(0,0,0,0.5)`. Mount/unmount is managed by the parent.
|
|
2632
|
+
*
|
|
2633
|
+
* @example
|
|
2634
|
+
* <Backdrop visible={isFadingIn} onClick={onClose} />
|
|
2635
|
+
*/
|
|
2636
|
+
const Backdrop = ({ visible, onClick }) => (jsx("div", { className: cx(BACKDROP_STYLES.root, visible && BACKDROP_STYLES.visible), onClick: onClick, "aria-hidden": true }));
|
|
2637
|
+
Backdrop.displayName = 'Backdrop';
|
|
2638
|
+
|
|
2639
|
+
/**
|
|
2640
|
+
* Manages mount/unmount transitions with a two-phase approach (visible → fading-in).
|
|
2641
|
+
*
|
|
2642
|
+
* Opening sequence:
|
|
2643
|
+
* 1. `useLayoutEffect` sets `isVisible=true` synchronously before the browser paints
|
|
2644
|
+
* → element is in the DOM at its initial hidden state on the very first frame.
|
|
2645
|
+
* 2. Double `requestAnimationFrame` in `useEffect` waits for two rendered frames
|
|
2646
|
+
* before setting `isFadingIn=true`, guaranteeing the CSS transition always starts
|
|
2647
|
+
* from the painted hidden state (prevents flickering).
|
|
2648
|
+
*
|
|
2649
|
+
* Closing sequence:
|
|
2650
|
+
* `isFadingIn=false` → CSS transition plays → after `duration` ms → `isVisible=false`.
|
|
2651
|
+
*
|
|
2652
|
+
* @param isOpen - Whether the element should be shown.
|
|
2653
|
+
* @param duration - Transition duration in milliseconds (defaults to `DEFAULT_TRANSITION_DURATION_MS`).
|
|
2654
|
+
*/
|
|
2655
|
+
const useTransitionRender = (isOpen, duration = DEFAULT_TRANSITION_DURATION_MS) => {
|
|
2656
|
+
const [isVisible, setIsVisible] = useState(isOpen);
|
|
2657
|
+
const [isFadingIn, setIsFadingIn] = useState(isOpen);
|
|
2658
|
+
// Mount synchronously before paint so the element is in the DOM at opacity:0
|
|
2659
|
+
// on the very first frame — no extra render cycle between null and the initial state.
|
|
2660
|
+
useLayoutEffect(() => {
|
|
2661
|
+
if (isOpen) {
|
|
2662
|
+
setIsVisible(true);
|
|
2663
|
+
}
|
|
2664
|
+
}, [isOpen]);
|
|
2665
|
+
useEffect(() => {
|
|
2666
|
+
if (isOpen) {
|
|
2667
|
+
// Double RAF: frame 1 → element rendered; frame 2 → element painted at hidden
|
|
2668
|
+
// state → THEN trigger the CSS transition.
|
|
2669
|
+
let raf2;
|
|
2670
|
+
const raf1 = requestAnimationFrame(() => {
|
|
2671
|
+
raf2 = requestAnimationFrame(() => setIsFadingIn(true));
|
|
2672
|
+
});
|
|
2673
|
+
return () => {
|
|
2674
|
+
cancelAnimationFrame(raf1);
|
|
2675
|
+
cancelAnimationFrame(raf2);
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
else {
|
|
2679
|
+
setIsFadingIn(false);
|
|
2680
|
+
const timeout = setTimeout(() => setIsVisible(false), duration);
|
|
2681
|
+
return () => clearTimeout(timeout);
|
|
2682
|
+
}
|
|
2683
|
+
}, [isOpen, duration]);
|
|
2684
|
+
return { isVisible, isFadingIn };
|
|
2685
|
+
};
|
|
2686
|
+
|
|
2687
|
+
/**
|
|
2688
|
+
* Locks scrolling on `document.body` while `active` is true.
|
|
2689
|
+
*
|
|
2690
|
+
* Preserves the current scroll position and keeps the scrollbar gutter
|
|
2691
|
+
* (`overflow-y: scroll`) to avoid horizontal layout shift when the scrollbar
|
|
2692
|
+
* disappears. The original styles and scroll position are restored on cleanup.
|
|
2693
|
+
*
|
|
2694
|
+
* @example useBodyScrollLock(isDialogOpen)
|
|
2695
|
+
*/
|
|
2696
|
+
const useBodyScrollLock = (active) => {
|
|
2697
|
+
useEffect(() => {
|
|
2698
|
+
if (!active) {
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
const scrollY = window.scrollY;
|
|
2702
|
+
const body = document.body;
|
|
2703
|
+
body.style.position = 'fixed';
|
|
2704
|
+
body.style.top = `-${scrollY}px`;
|
|
2705
|
+
body.style.overflowY = 'scroll';
|
|
2706
|
+
body.style.width = '100%';
|
|
2707
|
+
return () => {
|
|
2708
|
+
body.style.position = '';
|
|
2709
|
+
body.style.top = '';
|
|
2710
|
+
body.style.overflowY = '';
|
|
2711
|
+
body.style.width = '';
|
|
2712
|
+
window.scrollTo(0, scrollY);
|
|
2713
|
+
};
|
|
2714
|
+
}, [active]);
|
|
2715
|
+
};
|
|
2716
|
+
|
|
2717
|
+
const DrawerHeader = ({ children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }) => {
|
|
2718
|
+
return (jsx(Box, { px: 'sm', py: 'xs', role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...rest, children: children }));
|
|
2719
|
+
};
|
|
2720
|
+
|
|
2721
|
+
const DrawerBody = ({ children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }) => {
|
|
2722
|
+
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 }));
|
|
2723
|
+
};
|
|
2724
|
+
|
|
2725
|
+
const DrawerFooter = ({ children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }) => {
|
|
2726
|
+
return (jsx(Box, { px: 'sm', py: 'xs', role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...rest, children: children }));
|
|
2727
|
+
};
|
|
2728
|
+
|
|
2729
|
+
const DRAWER_ITEM_STYLES = createStyles((theme) => ({
|
|
2730
|
+
root: ({ selected, isExpanded }) => ({
|
|
2731
|
+
position: 'relative',
|
|
2732
|
+
display: 'flex',
|
|
2733
|
+
alignItems: 'center',
|
|
2734
|
+
width: isExpanded ? EXPANDED_DRAWER_ITEM_WIDTH : COLLAPSED_DRAWER_ITEM_WIDTH,
|
|
2735
|
+
gap: theme.spacing.sm,
|
|
2736
|
+
padding: `0 ${theme.spacing.smPlus}`,
|
|
2737
|
+
border: '1px solid transparent',
|
|
2738
|
+
borderRadius: theme.radius.md,
|
|
2739
|
+
fontFamily: 'inherit',
|
|
2740
|
+
fontSize: theme.fontSize.sm,
|
|
2741
|
+
userSelect: 'none',
|
|
2742
|
+
cursor: 'pointer',
|
|
2743
|
+
outline: 'none',
|
|
2744
|
+
textDecoration: 'none',
|
|
2745
|
+
boxSizing: 'border-box',
|
|
2746
|
+
transition: `width ${theme.transition.slow}, background-color ${theme.transition.fast}, color ${theme.transition.fast}`,
|
|
2747
|
+
whiteSpace: 'nowrap',
|
|
2748
|
+
overflow: 'hidden',
|
|
2749
|
+
height: DEFAULT_DRAWER_ITEM_SIZE,
|
|
2750
|
+
...(selected
|
|
2751
|
+
? {
|
|
2752
|
+
backgroundColor: theme.colors.primarySubtle,
|
|
2753
|
+
color: theme.colors.primaryMain,
|
|
2754
|
+
':hover': { backgroundColor: theme.colors.primarySubtleHover, color: theme.colors.primaryHover },
|
|
2755
|
+
':active': { backgroundColor: theme.colors.primarySubtleActive, color: theme.colors.primaryActive },
|
|
2756
|
+
}
|
|
2757
|
+
: {
|
|
2758
|
+
backgroundColor: 'transparent',
|
|
2759
|
+
color: theme.colors.defaultMain,
|
|
2760
|
+
':hover:not(:disabled)': { backgroundColor: theme.colors.defaultSubtleHover, color: theme.colors.defaultHover },
|
|
2761
|
+
':active:not(:disabled)': { backgroundColor: theme.colors.defaultSubtleActive, color: theme.colors.defaultActive },
|
|
2762
|
+
}),
|
|
2763
|
+
':focus-visible': { boxShadow: theme.shadows.focus },
|
|
2764
|
+
':disabled': { cursor: 'not-allowed', opacity: theme.opacity.high },
|
|
2765
|
+
}),
|
|
2766
|
+
iconWrap: {
|
|
2767
|
+
display: 'flex',
|
|
2768
|
+
alignItems: 'center',
|
|
2769
|
+
justifyContent: 'center',
|
|
2770
|
+
flexShrink: 0,
|
|
2771
|
+
},
|
|
2772
|
+
labelWrapper: ({ isFadingIn }) => ({
|
|
2773
|
+
display: 'flex',
|
|
2774
|
+
alignItems: 'center',
|
|
2775
|
+
flexDirection: 'row',
|
|
2776
|
+
gap: theme.spacing.sm,
|
|
2777
|
+
width: '100%',
|
|
2778
|
+
justifyContent: 'space-between',
|
|
2779
|
+
flex: 1,
|
|
2780
|
+
overflow: 'hidden',
|
|
2781
|
+
minWidth: 0,
|
|
2782
|
+
opacity: isFadingIn ? 1 : 0,
|
|
2783
|
+
transition: `opacity ${DEFAULT_TRANSITION_DURATION_MS}ms ease`,
|
|
2784
|
+
}),
|
|
2785
|
+
label: {
|
|
2786
|
+
flex: 1,
|
|
2787
|
+
overflow: 'hidden',
|
|
2788
|
+
textOverflow: 'ellipsis',
|
|
2789
|
+
fontWeight: theme.fontWeight.medium,
|
|
2790
|
+
},
|
|
2791
|
+
endContent: {
|
|
2792
|
+
display: 'flex',
|
|
2793
|
+
alignItems: 'center',
|
|
2794
|
+
flexShrink: 0,
|
|
2795
|
+
marginLeft: 'auto',
|
|
2796
|
+
},
|
|
2797
|
+
}));
|
|
2798
|
+
|
|
2799
|
+
/**
|
|
2800
|
+
* Navigation/action item for use inside Drawer.Body or Drawer.Footer.
|
|
2801
|
+
*
|
|
2802
|
+
* - In **expanded** mode: shows icon + label (fade-in) + optional end content.
|
|
2803
|
+
* - In **collapsed** mode: shows only the icon; the label fades out before unmounting
|
|
2804
|
+
* and appears as a right-side tooltip on hover.
|
|
2805
|
+
* - Renders as `<a>` when `href` is provided, otherwise as `<button>`.
|
|
2806
|
+
* - The `selected` prop applies a primary-color highlight.
|
|
2807
|
+
*/
|
|
2808
|
+
const DrawerItem = ({ startIcon, label, selected = false, endContent, href, onClick, disabled = false, ariaLabel, ariaLabelledBy, ariaDescribedBy, ariaControls, ariaExpanded, ariaHasPopup, ariaCurrent, }) => {
|
|
2809
|
+
const { isExpanded } = useDrawerContext();
|
|
2810
|
+
const { isVisible: isLabelVisible, isFadingIn: isLabelFadingIn } = useTransitionRender(isExpanded);
|
|
2811
|
+
const rootClassName = DRAWER_ITEM_STYLES.root({ selected, isExpanded });
|
|
2812
|
+
const computedAriaLabel = ariaLabel ?? (!isExpanded ? label : undefined);
|
|
2813
|
+
const computedAriaCurrent = ariaCurrent ?? (selected ? 'page' : undefined);
|
|
2814
|
+
const handleClick = (e) => {
|
|
2815
|
+
if (disabled) {
|
|
2816
|
+
e.preventDefault();
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
onClick?.();
|
|
2820
|
+
};
|
|
2821
|
+
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 }))] }))] }));
|
|
2822
|
+
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 }));
|
|
2823
|
+
return (jsx(Tooltip, { label: label, placement: 'right', inline: true, withArrow: true, disabled: isExpanded || disabled, children: item }));
|
|
2824
|
+
};
|
|
2825
|
+
DrawerItem.displayName = 'DrawerItem';
|
|
2826
|
+
|
|
2827
|
+
/**
|
|
2828
|
+
* Internal component: renders the temporary drawer as a fixed portal overlay
|
|
2829
|
+
* that slides in from the left with a backdrop, animated via `useTransitionRender`.
|
|
2830
|
+
*/
|
|
2831
|
+
const DrawerTemporaryPanel = ({ isExpanded, onClose, children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, }) => {
|
|
2832
|
+
const { isVisible, isFadingIn } = useTransitionRender(isExpanded);
|
|
2833
|
+
useBodyScrollLock(isExpanded);
|
|
2834
|
+
useKeyPress({ Escape: onClose }, { enabled: isExpanded });
|
|
2835
|
+
if (!isVisible) {
|
|
2836
|
+
return null;
|
|
2837
|
+
}
|
|
2838
|
+
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);
|
|
2839
|
+
};
|
|
2840
|
+
DrawerTemporaryPanel.displayName = 'DrawerTemporaryPanel';
|
|
2841
|
+
// ─── Main Drawer ─────────────────────────────────────────────────────────────
|
|
2842
|
+
/**
|
|
2843
|
+
* Collapsible side navigation drawer with controlled expanded/collapsed state.
|
|
2844
|
+
*
|
|
2845
|
+
* **Variants**
|
|
2846
|
+
* - `'permanent'` (default on desktop): inline drawer that pushes page content.
|
|
2847
|
+
* Toggles between `expanded` and `collapsed` states with animated width.
|
|
2848
|
+
* - `'temporary'` (default on mobile): portal overlay with a backdrop that slides
|
|
2849
|
+
* in from the left. `isExpanded` controls open/closed; `onClose` is called on
|
|
2850
|
+
* backdrop click or Escape.
|
|
2851
|
+
* - Omit `variant` to auto-detect based on viewport width.
|
|
2852
|
+
*
|
|
2853
|
+
* @example Permanent
|
|
2854
|
+
* <Drawer isExpanded={open} onExpandedChange={setOpen} height="100dvh">
|
|
2855
|
+
* <Drawer.Header>…</Drawer.Header>
|
|
2856
|
+
* <Drawer.Body>
|
|
2857
|
+
* <Drawer.Item startIcon={HomeIcon} label="Home" selected />
|
|
2858
|
+
* </Drawer.Body>
|
|
2859
|
+
* </Drawer>
|
|
2860
|
+
*
|
|
2861
|
+
* @example Temporary
|
|
2862
|
+
* <Drawer variant="temporary" isExpanded={open} onClose={() => setOpen(false)}>
|
|
2863
|
+
* …
|
|
2864
|
+
* </Drawer>
|
|
2865
|
+
*/
|
|
2866
|
+
const DrawerBase = ({ height = '100dvh', isExpanded, onExpandedChange, onClose, variant, children, role = 'navigation', ariaLabel = 'Navigation', ariaLabelledBy, ariaDescribedBy, }) => {
|
|
2867
|
+
const resolvedVariant = useDrawerVariant(variant);
|
|
2868
|
+
const handleClose = onClose ?? (() => onExpandedChange?.(false));
|
|
2869
|
+
if (resolvedVariant === 'temporary') {
|
|
2870
|
+
return (jsx(DrawerTemporaryPanel, { isExpanded: isExpanded, onClose: handleClose, height: height, role: role, ariaLabel: ariaLabel, ariaLabelledBy: ariaLabelledBy, ariaDescribedBy: ariaDescribedBy, children: children }));
|
|
2871
|
+
}
|
|
2872
|
+
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 }) }));
|
|
2873
|
+
};
|
|
2874
|
+
DrawerBase.displayName = 'Drawer';
|
|
2875
|
+
const Drawer = DrawerBase;
|
|
2876
|
+
Drawer.Header = DrawerHeader;
|
|
2877
|
+
Drawer.Body = DrawerBody;
|
|
2878
|
+
Drawer.Footer = DrawerFooter;
|
|
2879
|
+
Drawer.Item = DrawerItem;
|
|
2880
|
+
|
|
2337
2881
|
const AlertContext = createContext({
|
|
2338
2882
|
variant: 'default',
|
|
2339
2883
|
accentColor: 'defaultActive',
|
|
@@ -2405,31 +2949,8 @@ const Alert = AlertBase;
|
|
|
2405
2949
|
Alert.Title = AlertTitle;
|
|
2406
2950
|
Alert.Body = AlertBody;
|
|
2407
2951
|
|
|
2408
|
-
/**
|
|
2409
|
-
* Responsive breakpoints (min-width, mobile-first).
|
|
2410
|
-
* Keep in sync with themeBreakpoints.
|
|
2411
|
-
*/
|
|
2412
|
-
const BREAKPOINTS = {
|
|
2413
|
-
sm: 640};
|
|
2414
|
-
/** Max-width media query strings (max = breakpoint - 1px). */
|
|
2415
|
-
const MEDIA_MAX = {
|
|
2416
|
-
sm: `max-width: ${BREAKPOINTS.sm - 1}px`};
|
|
2417
|
-
|
|
2418
|
-
/** Default duration in milliseconds for mount/unmount transition animations. */
|
|
2419
|
-
const DEFAULT_TRANSITION_DURATION_MS = 250;
|
|
2420
|
-
|
|
2421
2952
|
const TRANSITION = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
|
|
2422
2953
|
const DIALOG_STYLES = createStyles((theme) => ({
|
|
2423
|
-
backdrop: {
|
|
2424
|
-
position: 'fixed',
|
|
2425
|
-
inset: 0,
|
|
2426
|
-
zIndex: theme.zIndex.modal - 1,
|
|
2427
|
-
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
2428
|
-
transition: `background-color ${TRANSITION}`,
|
|
2429
|
-
},
|
|
2430
|
-
backdropVisible: {
|
|
2431
|
-
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
2432
|
-
},
|
|
2433
2954
|
panel: {
|
|
2434
2955
|
position: 'fixed',
|
|
2435
2956
|
inset: 0,
|
|
@@ -2487,84 +3008,6 @@ const DialogContext = createContext({
|
|
|
2487
3008
|
CloseIconComponent: null,
|
|
2488
3009
|
});
|
|
2489
3010
|
|
|
2490
|
-
/**
|
|
2491
|
-
* Manages mount/unmount transitions with a two-phase approach (visible → fading-in).
|
|
2492
|
-
*
|
|
2493
|
-
* Opening sequence:
|
|
2494
|
-
* 1. `useLayoutEffect` sets `isVisible=true` synchronously before the browser paints
|
|
2495
|
-
* → element is in the DOM at its initial hidden state on the very first frame.
|
|
2496
|
-
* 2. Double `requestAnimationFrame` in `useEffect` waits for two rendered frames
|
|
2497
|
-
* before setting `isFadingIn=true`, guaranteeing the CSS transition always starts
|
|
2498
|
-
* from the painted hidden state (prevents flickering).
|
|
2499
|
-
*
|
|
2500
|
-
* Closing sequence:
|
|
2501
|
-
* `isFadingIn=false` → CSS transition plays → after `duration` ms → `isVisible=false`.
|
|
2502
|
-
*
|
|
2503
|
-
* @param isOpen - Whether the element should be shown.
|
|
2504
|
-
* @param duration - Transition duration in milliseconds (defaults to `DEFAULT_TRANSITION_DURATION_MS`).
|
|
2505
|
-
*/
|
|
2506
|
-
const useTransitionRender = (isOpen, duration = DEFAULT_TRANSITION_DURATION_MS) => {
|
|
2507
|
-
const [isVisible, setIsVisible] = useState(isOpen);
|
|
2508
|
-
const [isFadingIn, setIsFadingIn] = useState(isOpen);
|
|
2509
|
-
// Mount synchronously before paint so the element is in the DOM at opacity:0
|
|
2510
|
-
// on the very first frame — no extra render cycle between null and the initial state.
|
|
2511
|
-
useLayoutEffect(() => {
|
|
2512
|
-
if (isOpen) {
|
|
2513
|
-
setIsVisible(true);
|
|
2514
|
-
}
|
|
2515
|
-
}, [isOpen]);
|
|
2516
|
-
useEffect(() => {
|
|
2517
|
-
if (isOpen) {
|
|
2518
|
-
// Double RAF: frame 1 → element rendered; frame 2 → element painted at hidden
|
|
2519
|
-
// state → THEN trigger the CSS transition.
|
|
2520
|
-
let raf2;
|
|
2521
|
-
const raf1 = requestAnimationFrame(() => {
|
|
2522
|
-
raf2 = requestAnimationFrame(() => setIsFadingIn(true));
|
|
2523
|
-
});
|
|
2524
|
-
return () => {
|
|
2525
|
-
cancelAnimationFrame(raf1);
|
|
2526
|
-
cancelAnimationFrame(raf2);
|
|
2527
|
-
};
|
|
2528
|
-
}
|
|
2529
|
-
else {
|
|
2530
|
-
setIsFadingIn(false);
|
|
2531
|
-
const timeout = setTimeout(() => setIsVisible(false), duration);
|
|
2532
|
-
return () => clearTimeout(timeout);
|
|
2533
|
-
}
|
|
2534
|
-
}, [isOpen, duration]);
|
|
2535
|
-
return { isVisible, isFadingIn };
|
|
2536
|
-
};
|
|
2537
|
-
|
|
2538
|
-
/**
|
|
2539
|
-
* Locks scrolling on `document.body` while `active` is true.
|
|
2540
|
-
*
|
|
2541
|
-
* Preserves the current scroll position and keeps the scrollbar gutter
|
|
2542
|
-
* (`overflow-y: scroll`) to avoid horizontal layout shift when the scrollbar
|
|
2543
|
-
* disappears. The original styles and scroll position are restored on cleanup.
|
|
2544
|
-
*
|
|
2545
|
-
* @example useBodyScrollLock(isDialogOpen)
|
|
2546
|
-
*/
|
|
2547
|
-
const useBodyScrollLock = (active) => {
|
|
2548
|
-
useEffect(() => {
|
|
2549
|
-
if (!active) {
|
|
2550
|
-
return;
|
|
2551
|
-
}
|
|
2552
|
-
const scrollY = window.scrollY;
|
|
2553
|
-
const body = document.body;
|
|
2554
|
-
body.style.position = 'fixed';
|
|
2555
|
-
body.style.top = `-${scrollY}px`;
|
|
2556
|
-
body.style.overflowY = 'scroll';
|
|
2557
|
-
body.style.width = '100%';
|
|
2558
|
-
return () => {
|
|
2559
|
-
body.style.position = '';
|
|
2560
|
-
body.style.top = '';
|
|
2561
|
-
body.style.overflowY = '';
|
|
2562
|
-
body.style.width = '';
|
|
2563
|
-
window.scrollTo(0, scrollY);
|
|
2564
|
-
};
|
|
2565
|
-
}, [active]);
|
|
2566
|
-
};
|
|
2567
|
-
|
|
2568
3011
|
const FOCUSABLE_SELECTOR = [
|
|
2569
3012
|
'a[href]',
|
|
2570
3013
|
'button:not([disabled])',
|
|
@@ -2700,7 +3143,7 @@ const DialogBase = ({ open, onClose, children, closeOnBackdropClick = false, ful
|
|
|
2700
3143
|
if (!isVisible) {
|
|
2701
3144
|
return null;
|
|
2702
3145
|
}
|
|
2703
|
-
return createPortal(jsxs(Fragment$1, { children: [jsx(
|
|
3146
|
+
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);
|
|
2704
3147
|
};
|
|
2705
3148
|
DialogBase.displayName = 'Dialog';
|
|
2706
3149
|
const Dialog = DialogBase;
|
|
@@ -2911,13 +3354,19 @@ const themeShadows = {
|
|
|
2911
3354
|
|
|
2912
3355
|
/**
|
|
2913
3356
|
* Default spacing tokens
|
|
3357
|
+
*
|
|
3358
|
+
* ⚠️ Token keys MUST stay CSS-custom-property safe (letters, digits, hyphens only).
|
|
3359
|
+
* They are turned into CSS variables (e.g. `smPlus` → `--theme-spacing-sm-plus`) by
|
|
3360
|
+
* the theme proxy, so characters like `+` would produce invalid variable names and
|
|
3361
|
+
* silently break any style that uses them.
|
|
2914
3362
|
*/
|
|
2915
3363
|
const themeSpacing = {
|
|
2916
3364
|
none: '0',
|
|
2917
3365
|
'2xs': '0.125rem', // 2px
|
|
2918
3366
|
xs: '0.25rem', // 4px
|
|
2919
|
-
|
|
3367
|
+
xsPlus: '0.375rem', // 6px
|
|
2920
3368
|
sm: '0.5rem', // 8px
|
|
3369
|
+
smPlus: '0.75rem', // 12px
|
|
2921
3370
|
md: '1rem', // 16px
|
|
2922
3371
|
lg: '1.5rem', // 24px
|
|
2923
3372
|
xl: '2rem', // 32px
|
|
@@ -2931,9 +3380,9 @@ const themeSpacing = {
|
|
|
2931
3380
|
* Default transition tokens
|
|
2932
3381
|
*/
|
|
2933
3382
|
const themeTransition = {
|
|
2934
|
-
fast: '150ms ease-out',
|
|
2935
|
-
normal: '250ms ease-out',
|
|
2936
|
-
slow: '350ms ease-out',
|
|
3383
|
+
fast: '150ms ease-in-out',
|
|
3384
|
+
normal: '250ms ease-in-out',
|
|
3385
|
+
slow: '350ms ease-in-out',
|
|
2937
3386
|
};
|
|
2938
3387
|
|
|
2939
3388
|
/**
|
|
@@ -3088,5 +3537,5 @@ const darkTheme = createTheme({
|
|
|
3088
3537
|
breakpoints: themeBreakpoints,
|
|
3089
3538
|
});
|
|
3090
3539
|
|
|
3091
|
-
export { Alert, Badge, Box, Button, Card, Dialog, Form, Grid, Icon, IconButton, InfoBubble, Link, Menu, Select, Skeleton, Stack, Switch, Text, TextField, Tooltip, darkTheme, lightTheme };
|
|
3540
|
+
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 };
|
|
3092
3541
|
//# sourceMappingURL=index.js.map
|