@aurora-ds/components 1.1.6 → 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 CHANGED
@@ -181,24 +181,32 @@ const skeletonShimmerAnimation = theme.keyframes({
181
181
  '100%': { backgroundPosition: '0% 50%' },
182
182
  });
183
183
 
184
- const APPEARANCES = ['contained', 'outlined', 'text'];
185
- const buildActionButtonVariantBase = (theme) => ({
186
- position: 'relative',
187
- display: 'inline-flex',
188
- alignItems: 'center',
189
- justifyContent: 'center',
190
- boxSizing: 'border-box',
191
- border: '1px solid transparent',
192
- borderRadius: theme.radius.md,
193
- fontFamily: 'inherit',
194
- userSelect: 'none',
195
- cursor: 'pointer',
196
- outline: 'none',
197
- transition: `background-color ${theme.transition.fast}, border-color ${theme.transition.fast}, color ${theme.transition.fast}, box-shadow ${theme.transition.fast}`,
198
- ':focus-visible': { boxShadow: theme.shadows.focus },
199
- ':disabled': { cursor: 'not-allowed', opacity: theme.opacity.high, boxShadow: 'none' },
200
- });
201
- const buildActionButtonCompoundVariants = (theme) => {
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 appearanceStyles = (intent, appearance) => {
241
- if (appearance === 'contained') {
242
- return {
243
- backgroundColor: intent.main,
244
- borderColor: intent.main,
245
- color: intent.on,
246
- boxShadow: theme.shadows.xs,
247
- ':hover:not(:disabled)': { backgroundColor: intent.hover, borderColor: intent.hover, boxShadow: theme.shadows.sm },
248
- ':active:not(:disabled)': { backgroundColor: intent.active, borderColor: intent.active, boxShadow: theme.shadows.none },
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
- if (appearance === 'outlined') {
252
- return {
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
- return {
261
- backgroundColor: 'transparent',
262
- borderColor: 'transparent',
263
- color: intent.fg,
264
- ':hover:not(:disabled)': { backgroundColor: intent.subtleHover, color: intent.fgHover },
265
- ':active:not(:disabled)': { backgroundColor: intent.subtleActive, color: intent.active },
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 BUTTON_VARIANTS = theme.createVariants((theme) => ({
276
- base: buildActionButtonVariantBase(theme),
277
- variants: {
278
- size: {
279
- sm: { height: '2rem', padding: `0 ${theme.spacing.sm}`, fontSize: theme.fontSize.xs },
280
- md: { height: '2.5rem', padding: `0 ${theme.spacing.md}`, fontSize: theme.fontSize.sm },
281
- lg: { height: '3rem', padding: `0 ${theme.spacing.lg}`, fontSize: theme.fontSize.md },
282
- },
283
- // Appearance/color styling is provided entirely by compoundVariants.
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-extra' });
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 }) => ({
@@ -579,7 +597,7 @@ const ICON_SIZE$2 = {
579
597
  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 }) => {
580
598
  const isDisabled = disabled || isLoading;
581
599
  const iconSize = ICON_SIZE$2[size];
582
- const rootClassName = BUTTON_VARIANTS({ variant, color, size }, className);
600
+ const rootClassName = theme.cx(BUTTON_STYLES.root({ variant, color, size }), className);
583
601
  const mergedStyle = {
584
602
  ...style,
585
603
  ...(width !== undefined ? { width } : {}),
@@ -590,21 +608,15 @@ const Button = ({ ref, variant = 'contained', color = 'primary', size = 'md', wi
590
608
  };
591
609
  Button.displayName = 'Button';
592
610
 
593
- const ICON_BUTTON_VARIANTS = theme.createVariants((theme) => ({
594
- base: buildActionButtonVariantBase(theme),
595
- variants: {
596
- size: {
597
- sm: { width: '2rem', height: '2rem', padding: '0' },
598
- md: { width: '2.5rem', height: '2.5rem', padding: '0' },
599
- lg: { width: '3rem', height: '3rem', padding: '0' },
600
- },
601
- variant: { contained: {}, outlined: {}, text: {} },
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-extra' });
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 = ICON_BUTTON_VARIANTS({ variant, color, size }, className);
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';
@@ -855,7 +871,7 @@ const BADGE_VARIANTS = theme.createVariants((theme) => ({
855
871
  size: {
856
872
  sm: {
857
873
  height: '1.25rem',
858
- padding: `0.125rem ${theme.spacing['xs+']}`,
874
+ padding: `0.125rem ${theme.spacing.xsPlus}`,
859
875
  fontSize: theme.fontSize['2xs'],
860
876
  },
861
877
  md: {
@@ -1585,6 +1601,18 @@ Box.displayName = 'Box';
1585
1601
  const Stack = ({ flexDirection = 'row', display = 'flex', gap = 'sm', ...rest }) => (jsxRuntime.jsx(Box, { display: display, flexDirection: flexDirection, gap: gap, ...rest }));
1586
1602
  Stack.displayName = 'Stack';
1587
1603
 
1604
+ const HELPER_COLOR_MAP = {
1605
+ default: 'textSecondary',
1606
+ error: 'errorHover',
1607
+ success: 'successHover',
1608
+ warning: 'warningHover',
1609
+ };
1610
+ const FormHelperText = ({ id, status = 'default', ariaLive, className, children, }) => {
1611
+ const resolvedAriaLive = ariaLive ?? (status === 'error' ? 'assertive' : 'polite');
1612
+ return (jsxRuntime.jsx(Text, { id: id, variant: 'span', fontSize: 'xs', color: HELPER_COLOR_MAP[status], "aria-live": resolvedAriaLive, className: className, children: children }));
1613
+ };
1614
+ FormHelperText.displayName = 'FormHelperText';
1615
+
1588
1616
  const TEXTFIELD_WRAPPER_VARIANTS = theme.createVariants((theme) => {
1589
1617
  const c = theme.colors;
1590
1618
  return {
@@ -1707,18 +1735,11 @@ const ICON_BUTTON_SIZE_MAP = {
1707
1735
  md: 'sm',
1708
1736
  lg: 'md',
1709
1737
  };
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
1738
  /**
1718
1739
  * Business logic for the TextField component: id resolution, ref merging,
1719
1740
  * password visibility toggling and size/status derived tokens.
1720
1741
  */
1721
- const useTextField = ({ id, ref, type, size, status, endAction, }) => {
1742
+ const useTextField = ({ id, ref, type, size, endAction, }) => {
1722
1743
  const generatedId = React.useId();
1723
1744
  const fieldId = id ?? generatedId;
1724
1745
  const helperId = `${fieldId}-helper`;
@@ -1740,7 +1761,6 @@ const useTextField = ({ id, ref, type, size, status, endAction, }) => {
1740
1761
  resolvedType,
1741
1762
  iconSize: ICON_SIZE_MAP$1[size],
1742
1763
  iconButtonSize: ICON_BUTTON_SIZE_MAP[size],
1743
- helperColor: HELPER_COLOR_MAP$1[status],
1744
1764
  hasEndSection: endAction !== undefined || isPassword,
1745
1765
  focusInput,
1746
1766
  };
@@ -1758,8 +1778,8 @@ const useTextField = ({ id, ref, type, size, status, endAction, }) => {
1758
1778
  * @example <TextField label="Search" startIcon={SearchIcon} endAction={<ClearButton />} />
1759
1779
  */
1760
1780
  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, helperColor, hasEndSection, focusInput, } = useTextField({ id, ref, type, size, status, endAction });
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', tabIndex: -1, onClick: togglePassword }))] }))] }), helperText !== undefined && (jsxRuntime.jsx(Text, { id: helperId, variant: 'span', fontSize: 'xs', color: helperColor, children: helperText }))] }));
1781
+ const { fieldId, helperId, mergedRef, isPassword, showPassword, togglePassword, resolvedType, iconSize, iconButtonSize, hasEndSection, focusInput, } = useTextField({ id, ref, type, size, endAction });
1782
+ 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', tabIndex: -1, onClick: togglePassword }))] }))] }), helperText !== undefined && (jsxRuntime.jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
1763
1783
  };
1764
1784
  TextField.displayName = 'TextField';
1765
1785
 
@@ -2195,21 +2215,15 @@ const SelectTrigger = ({ ref, size = 'md', status = 'default', open, hasValue, s
2195
2215
  };
2196
2216
  SelectTrigger.displayName = 'SelectTrigger';
2197
2217
 
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
2218
  /**
2206
2219
  * Business logic for the Select component: id resolution, ref merging,
2207
2220
  * controlled/uncontrolled value handling, open state, option grouping and
2208
2221
  * focus restoration to the trigger when the menu closes.
2209
2222
  */
2210
- const useSelect = ({ id, ref, value, defaultValue, onChange, options, status, disabled, }) => {
2223
+ const useSelect = ({ id, ref, value, defaultValue, onChange, options, disabled, }) => {
2211
2224
  const generatedId = React.useId();
2212
2225
  const fieldId = id ?? generatedId;
2226
+ const helperId = `${fieldId}-helper`;
2213
2227
  const menuId = `${fieldId}-menu`;
2214
2228
  const triggerRef = React.useRef(null);
2215
2229
  const mergedRef = useMergedRefs(ref, triggerRef);
@@ -2254,6 +2268,7 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, status, di
2254
2268
  const close = React.useCallback(() => setOpen(false), []);
2255
2269
  return {
2256
2270
  fieldId,
2271
+ helperId,
2257
2272
  menuId,
2258
2273
  triggerRef,
2259
2274
  mergedRef,
@@ -2264,19 +2279,187 @@ const useSelect = ({ id, ref, value, defaultValue, onChange, options, status, di
2264
2279
  selectedOption,
2265
2280
  groupedOptions,
2266
2281
  handleSelect,
2267
- helperColor: HELPER_COLOR_MAP[status],
2268
2282
  };
2269
2283
  };
2270
2284
 
2271
2285
  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, helperColor, } = useSelect({ id, ref, value, defaultValue, onChange, options, status, disabled });
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, required: required, "aria-haspopup": 'listbox', "aria-expanded": open, "aria-controls": menuId, "aria-invalid": status === 'error' || undefined, onClick: toggle, children: selectedOption !== undefined ? selectedOption.label : placeholder }), jsxRuntime.jsx(Menu, { open: open, onClose: close, anchorEl: triggerRef.current, id: menuId, children: Array.from(groupedOptions.entries()).map(([groupKey, groupOpts], groupIndex) => {
2286
+ const { fieldId, helperId, menuId, triggerRef, mergedRef, open, toggle, close, currentValue, selectedOption, groupedOptions, handleSelect, } = useSelect({ id, ref, value, defaultValue, onChange, options, disabled });
2287
+ 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, 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 }), jsxRuntime.jsx(Menu, { open: open, onClose: close, anchorEl: triggerRef.current, id: menuId, children: Array.from(groupedOptions.entries()).map(([groupKey, groupOpts], groupIndex) => {
2274
2288
  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
2289
  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(Text, { variant: 'span', fontSize: 'xs', color: helperColor, children: helperText }))] }));
2290
+ }) }), helperText !== undefined && (jsxRuntime.jsx(FormHelperText, { id: helperId, status: status, children: helperText }))] }));
2277
2291
  };
2278
2292
  Select.displayName = 'Select';
2279
2293
 
2294
+ const CHECKBOX_ROOT_VARIANTS = theme.createVariants((theme) => ({
2295
+ base: {
2296
+ display: 'inline-flex',
2297
+ alignItems: 'center',
2298
+ gap: theme.spacing.sm,
2299
+ cursor: 'pointer',
2300
+ userSelect: 'none',
2301
+ },
2302
+ variants: {
2303
+ disabled: {
2304
+ true: {
2305
+ cursor: 'not-allowed',
2306
+ opacity: theme.opacity.high,
2307
+ },
2308
+ false: {},
2309
+ },
2310
+ },
2311
+ defaultVariants: {
2312
+ disabled: 'false',
2313
+ },
2314
+ }), { id: 'checkbox-root' });
2315
+ const CHECKBOX_INPUT_VARIANTS = theme.createVariants((theme) => {
2316
+ const c = theme.colors;
2317
+ return {
2318
+ base: {
2319
+ appearance: 'none',
2320
+ position: 'relative',
2321
+ margin: 0,
2322
+ borderWidth: '1px',
2323
+ borderStyle: 'solid',
2324
+ borderColor: c.borderStrong,
2325
+ borderRadius: theme.radius.sm,
2326
+ backgroundColor: c.surfacePaper,
2327
+ transition: `border-color ${theme.transition.fast}, background-color ${theme.transition.fast}`,
2328
+ cursor: 'pointer',
2329
+ '&:focus-visible': {
2330
+ outline: '2px solid transparent',
2331
+ boxShadow: `0 0 0 2px ${c.primarySubtleActive}`,
2332
+ },
2333
+ '&::after': {
2334
+ content: '""',
2335
+ position: 'absolute',
2336
+ left: '50%',
2337
+ top: '50%',
2338
+ transform: 'translate(-50%, -56%) rotate(45deg)',
2339
+ width: '0.25rem',
2340
+ height: '0.5rem',
2341
+ borderRight: `2px solid ${c.textInverse}`,
2342
+ borderBottom: `2px solid ${c.textInverse}`,
2343
+ opacity: 0,
2344
+ },
2345
+ '&:checked::after': {
2346
+ opacity: 1,
2347
+ },
2348
+ '&:indeterminate::after': {
2349
+ width: '0.5rem',
2350
+ height: '0',
2351
+ borderRight: '0',
2352
+ borderBottom: `2px solid ${c.textInverse}`,
2353
+ transform: 'translate(-50%, -50%)',
2354
+ opacity: 1,
2355
+ },
2356
+ },
2357
+ variants: {
2358
+ size: {
2359
+ sm: {
2360
+ width: '1rem',
2361
+ height: '1rem',
2362
+ },
2363
+ md: {
2364
+ width: '1.125rem',
2365
+ height: '1.125rem',
2366
+ },
2367
+ lg: {
2368
+ width: '1.25rem',
2369
+ height: '1.25rem',
2370
+ },
2371
+ },
2372
+ status: {
2373
+ default: {
2374
+ '&:checked, &:indeterminate': {
2375
+ borderColor: c.primaryMain,
2376
+ backgroundColor: c.primaryMain,
2377
+ },
2378
+ },
2379
+ error: {
2380
+ borderColor: c.errorMain,
2381
+ '&:checked, &:indeterminate': {
2382
+ borderColor: c.errorMain,
2383
+ backgroundColor: c.errorMain,
2384
+ },
2385
+ },
2386
+ success: {
2387
+ borderColor: c.successMain,
2388
+ '&:checked, &:indeterminate': {
2389
+ borderColor: c.successMain,
2390
+ backgroundColor: c.successMain,
2391
+ },
2392
+ },
2393
+ warning: {
2394
+ borderColor: c.warningMain,
2395
+ '&:checked, &:indeterminate': {
2396
+ borderColor: c.warningMain,
2397
+ backgroundColor: c.warningMain,
2398
+ },
2399
+ },
2400
+ },
2401
+ disabled: {
2402
+ true: {
2403
+ cursor: 'not-allowed',
2404
+ backgroundColor: c.disabledMain,
2405
+ borderColor: c.disabledMain,
2406
+ '&:checked, &:indeterminate': {
2407
+ backgroundColor: c.disabledText,
2408
+ borderColor: c.disabledText,
2409
+ },
2410
+ },
2411
+ false: {},
2412
+ },
2413
+ },
2414
+ defaultVariants: {
2415
+ size: 'md',
2416
+ status: 'default',
2417
+ disabled: 'false',
2418
+ },
2419
+ };
2420
+ }, { id: 'checkbox-input' });
2421
+ const CHECKBOX_STYLES = theme.createStyles((theme) => ({
2422
+ wrapper: {
2423
+ display: 'inline-flex',
2424
+ flexDirection: 'column',
2425
+ gap: theme.spacing.xs,
2426
+ },
2427
+ helper: {
2428
+ marginLeft: `calc(1.125rem + ${theme.spacing.sm})`,
2429
+ },
2430
+ }), { id: 'checkbox-extra' });
2431
+
2432
+ /** Handles id generation, ref merging and native indeterminate state sync. */
2433
+ const useCheckbox = ({ id, ref, indeterminate = false }) => {
2434
+ const generatedId = React.useId();
2435
+ const checkboxId = id ?? generatedId;
2436
+ const helperId = `${checkboxId}-helper`;
2437
+ const inputRef = React.useRef(null);
2438
+ const mergedRef = useMergedRefs(ref, inputRef);
2439
+ React.useEffect(() => {
2440
+ if (inputRef.current) {
2441
+ inputRef.current.indeterminate = indeterminate;
2442
+ }
2443
+ }, [indeterminate]);
2444
+ return {
2445
+ checkboxId,
2446
+ helperId,
2447
+ mergedRef,
2448
+ inputRef,
2449
+ };
2450
+ };
2451
+
2452
+ const Checkbox = ({ ref, label, helperText, size = 'md', status = 'default', indeterminate = false, error, id, disabled, required, ...rest }) => {
2453
+ const resolvedStatus = error ? 'error' : status;
2454
+ const { checkboxId, helperId, mergedRef } = useCheckbox({ id, ref, indeterminate });
2455
+ 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, className: CHECKBOX_INPUT_VARIANTS({
2456
+ size,
2457
+ status: resolvedStatus,
2458
+ disabled: disabled ? 'true' : 'false',
2459
+ }), ...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 }))] }));
2460
+ };
2461
+ Checkbox.displayName = 'Checkbox';
2462
+
2280
2463
  const CARD_VARIANTS = theme.createVariants((theme) => ({
2281
2464
  base: {
2282
2465
  boxSizing: 'border-box',
@@ -2360,6 +2543,361 @@ const Grid = ({ display = 'grid', columns, rows, autoFlow, autoColumns, autoRows
2360
2543
  };
2361
2544
  Grid.displayName = 'Grid';
2362
2545
 
2546
+ const DrawerContext = React.createContext({
2547
+ isExpanded: true,
2548
+ });
2549
+ const useDrawerContext = () => React.useContext(DrawerContext);
2550
+
2551
+ const TRANSITION$2 = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
2552
+ const DRAWER_STYLES = theme.createStyles((theme) => ({
2553
+ root: ({ isExpanded }) => ({
2554
+ display: 'flex',
2555
+ flexDirection: 'column',
2556
+ width: isExpanded ? EXPANDED_DRAWER_WIDTH : COLLAPSED_DRAWER_WIDTH,
2557
+ transition: `width ${theme.transition.slow}`,
2558
+ overflow: 'hidden',
2559
+ backgroundColor: theme.colors.surfacePaper,
2560
+ borderRight: `1px solid ${theme.colors.borderMain}`,
2561
+ boxSizing: 'border-box',
2562
+ flexShrink: 0,
2563
+ }),
2564
+ /** Temporary variant: slides in from the left as a fixed portal overlay. */
2565
+ temporaryPanel: {
2566
+ position: 'fixed',
2567
+ top: 0,
2568
+ left: 0,
2569
+ bottom: 0,
2570
+ width: EXPANDED_DRAWER_WIDTH,
2571
+ zIndex: theme.zIndex.modal,
2572
+ display: 'flex',
2573
+ flexDirection: 'column',
2574
+ backgroundColor: theme.colors.surfacePaper,
2575
+ borderRight: `1px solid ${theme.colors.borderMain}`,
2576
+ boxSizing: 'border-box',
2577
+ overflowY: 'auto',
2578
+ overflowX: 'hidden',
2579
+ willChange: 'transform',
2580
+ transform: 'translateX(-100%)',
2581
+ transition: `transform ${TRANSITION$2}`,
2582
+ boxShadow: theme.shadows.xl,
2583
+ },
2584
+ temporaryPanelVisible: {
2585
+ transform: 'translateX(0)',
2586
+ },
2587
+ }));
2588
+
2589
+ /**
2590
+ * Responsive breakpoints (min-width, mobile-first).
2591
+ * Keep in sync with themeBreakpoints.
2592
+ */
2593
+ const BREAKPOINTS = {
2594
+ sm: 640};
2595
+ /** Max-width media query strings (max = breakpoint - 1px). */
2596
+ const MEDIA_MAX = {
2597
+ sm: `max-width: ${BREAKPOINTS.sm - 1}px`};
2598
+
2599
+ const MOBILE_MQ = `(max-width: ${BREAKPOINTS.sm - 1}px)`;
2600
+ /**
2601
+ * Resolves the effective drawer variant based on the explicit `variant` prop
2602
+ * and the current viewport width.
2603
+ *
2604
+ * - If `variant` is explicitly provided (`'permanent'` or `'temporary'`), returns it as-is.
2605
+ * - Otherwise, auto-detects: `'temporary'` on mobile (< sm breakpoint), `'permanent'` on desktop.
2606
+ *
2607
+ * Reacts to viewport changes (window resize) so switching between mobile and desktop
2608
+ * automatically updates the variant without requiring a page reload.
2609
+ */
2610
+ const useDrawerVariant = (variant) => {
2611
+ const [isMobile, setIsMobile] = React.useState(() => {
2612
+ if (typeof window === 'undefined') {
2613
+ return false;
2614
+ }
2615
+ return window.matchMedia(MOBILE_MQ).matches;
2616
+ });
2617
+ React.useEffect(() => {
2618
+ if (typeof window === 'undefined') {
2619
+ return;
2620
+ }
2621
+ const mq = window.matchMedia(MOBILE_MQ);
2622
+ const handler = (e) => setIsMobile(e.matches);
2623
+ mq.addEventListener('change', handler);
2624
+ return () => mq.removeEventListener('change', handler);
2625
+ }, []);
2626
+ if (variant !== undefined) {
2627
+ return variant;
2628
+ }
2629
+ return isMobile ? 'temporary' : 'permanent';
2630
+ };
2631
+
2632
+ const TRANSITION$1 = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
2633
+ const BACKDROP_STYLES = theme.createStyles((theme) => ({
2634
+ root: {
2635
+ position: 'fixed',
2636
+ inset: 0,
2637
+ zIndex: theme.zIndex.modal - 1,
2638
+ backgroundColor: 'rgba(0, 0, 0, 0)',
2639
+ transition: `background-color ${TRANSITION$1}`,
2640
+ },
2641
+ visible: {
2642
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
2643
+ },
2644
+ }), { id: 'backdrop' });
2645
+
2646
+ /**
2647
+ * Semi-transparent full-screen overlay used to visually block page content
2648
+ * while a modal element (dialog, temporary drawer…) is open.
2649
+ *
2650
+ * The `visible` prop drives the opacity transition: `false` = transparent,
2651
+ * `true` = `rgba(0,0,0,0.5)`. Mount/unmount is managed by the parent.
2652
+ *
2653
+ * @example
2654
+ * <Backdrop visible={isFadingIn} onClick={onClose} />
2655
+ */
2656
+ const Backdrop = ({ visible, onClick }) => (jsxRuntime.jsx("div", { className: theme.cx(BACKDROP_STYLES.root, visible && BACKDROP_STYLES.visible), onClick: onClick, "aria-hidden": true }));
2657
+ Backdrop.displayName = 'Backdrop';
2658
+
2659
+ /**
2660
+ * Manages mount/unmount transitions with a two-phase approach (visible → fading-in).
2661
+ *
2662
+ * Opening sequence:
2663
+ * 1. `useLayoutEffect` sets `isVisible=true` synchronously before the browser paints
2664
+ * → element is in the DOM at its initial hidden state on the very first frame.
2665
+ * 2. Double `requestAnimationFrame` in `useEffect` waits for two rendered frames
2666
+ * before setting `isFadingIn=true`, guaranteeing the CSS transition always starts
2667
+ * from the painted hidden state (prevents flickering).
2668
+ *
2669
+ * Closing sequence:
2670
+ * `isFadingIn=false` → CSS transition plays → after `duration` ms → `isVisible=false`.
2671
+ *
2672
+ * @param isOpen - Whether the element should be shown.
2673
+ * @param duration - Transition duration in milliseconds (defaults to `DEFAULT_TRANSITION_DURATION_MS`).
2674
+ */
2675
+ const useTransitionRender = (isOpen, duration = DEFAULT_TRANSITION_DURATION_MS) => {
2676
+ const [isVisible, setIsVisible] = React.useState(isOpen);
2677
+ const [isFadingIn, setIsFadingIn] = React.useState(isOpen);
2678
+ // Mount synchronously before paint so the element is in the DOM at opacity:0
2679
+ // on the very first frame — no extra render cycle between null and the initial state.
2680
+ React.useLayoutEffect(() => {
2681
+ if (isOpen) {
2682
+ setIsVisible(true);
2683
+ }
2684
+ }, [isOpen]);
2685
+ React.useEffect(() => {
2686
+ if (isOpen) {
2687
+ // Double RAF: frame 1 → element rendered; frame 2 → element painted at hidden
2688
+ // state → THEN trigger the CSS transition.
2689
+ let raf2;
2690
+ const raf1 = requestAnimationFrame(() => {
2691
+ raf2 = requestAnimationFrame(() => setIsFadingIn(true));
2692
+ });
2693
+ return () => {
2694
+ cancelAnimationFrame(raf1);
2695
+ cancelAnimationFrame(raf2);
2696
+ };
2697
+ }
2698
+ else {
2699
+ setIsFadingIn(false);
2700
+ const timeout = setTimeout(() => setIsVisible(false), duration);
2701
+ return () => clearTimeout(timeout);
2702
+ }
2703
+ }, [isOpen, duration]);
2704
+ return { isVisible, isFadingIn };
2705
+ };
2706
+
2707
+ /**
2708
+ * Locks scrolling on `document.body` while `active` is true.
2709
+ *
2710
+ * Preserves the current scroll position and keeps the scrollbar gutter
2711
+ * (`overflow-y: scroll`) to avoid horizontal layout shift when the scrollbar
2712
+ * disappears. The original styles and scroll position are restored on cleanup.
2713
+ *
2714
+ * @example useBodyScrollLock(isDialogOpen)
2715
+ */
2716
+ const useBodyScrollLock = (active) => {
2717
+ React.useEffect(() => {
2718
+ if (!active) {
2719
+ return;
2720
+ }
2721
+ const scrollY = window.scrollY;
2722
+ const body = document.body;
2723
+ body.style.position = 'fixed';
2724
+ body.style.top = `-${scrollY}px`;
2725
+ body.style.overflowY = 'scroll';
2726
+ body.style.width = '100%';
2727
+ return () => {
2728
+ body.style.position = '';
2729
+ body.style.top = '';
2730
+ body.style.overflowY = '';
2731
+ body.style.width = '';
2732
+ window.scrollTo(0, scrollY);
2733
+ };
2734
+ }, [active]);
2735
+ };
2736
+
2737
+ const DrawerHeader = ({ children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }) => {
2738
+ return (jsxRuntime.jsx(Box, { px: 'sm', py: 'xs', role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...rest, children: children }));
2739
+ };
2740
+
2741
+ const DrawerBody = ({ children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }) => {
2742
+ 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 }));
2743
+ };
2744
+
2745
+ const DrawerFooter = ({ children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, ...rest }) => {
2746
+ return (jsxRuntime.jsx(Box, { px: 'sm', py: 'xs', role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...rest, children: children }));
2747
+ };
2748
+
2749
+ const DRAWER_ITEM_STYLES = theme.createStyles((theme) => ({
2750
+ root: ({ selected, isExpanded }) => ({
2751
+ position: 'relative',
2752
+ display: 'flex',
2753
+ alignItems: 'center',
2754
+ width: isExpanded ? EXPANDED_DRAWER_ITEM_WIDTH : COLLAPSED_DRAWER_ITEM_WIDTH,
2755
+ gap: theme.spacing.sm,
2756
+ padding: `0 ${theme.spacing.smPlus}`,
2757
+ border: '1px solid transparent',
2758
+ borderRadius: theme.radius.md,
2759
+ fontFamily: 'inherit',
2760
+ fontSize: theme.fontSize.sm,
2761
+ userSelect: 'none',
2762
+ cursor: 'pointer',
2763
+ outline: 'none',
2764
+ textDecoration: 'none',
2765
+ boxSizing: 'border-box',
2766
+ transition: `width ${theme.transition.slow}, background-color ${theme.transition.fast}, color ${theme.transition.fast}`,
2767
+ whiteSpace: 'nowrap',
2768
+ overflow: 'hidden',
2769
+ height: DEFAULT_DRAWER_ITEM_SIZE,
2770
+ ...(selected
2771
+ ? {
2772
+ backgroundColor: theme.colors.primarySubtle,
2773
+ color: theme.colors.primaryMain,
2774
+ ':hover': { backgroundColor: theme.colors.primarySubtleHover, color: theme.colors.primaryHover },
2775
+ ':active': { backgroundColor: theme.colors.primarySubtleActive, color: theme.colors.primaryActive },
2776
+ }
2777
+ : {
2778
+ backgroundColor: 'transparent',
2779
+ color: theme.colors.defaultMain,
2780
+ ':hover:not(:disabled)': { backgroundColor: theme.colors.defaultSubtleHover, color: theme.colors.defaultHover },
2781
+ ':active:not(:disabled)': { backgroundColor: theme.colors.defaultSubtleActive, color: theme.colors.defaultActive },
2782
+ }),
2783
+ ':focus-visible': { boxShadow: theme.shadows.focus },
2784
+ ':disabled': { cursor: 'not-allowed', opacity: theme.opacity.high },
2785
+ }),
2786
+ iconWrap: {
2787
+ display: 'flex',
2788
+ alignItems: 'center',
2789
+ justifyContent: 'center',
2790
+ flexShrink: 0,
2791
+ },
2792
+ labelWrapper: ({ isFadingIn }) => ({
2793
+ display: 'flex',
2794
+ alignItems: 'center',
2795
+ flexDirection: 'row',
2796
+ gap: theme.spacing.sm,
2797
+ width: '100%',
2798
+ justifyContent: 'space-between',
2799
+ flex: 1,
2800
+ overflow: 'hidden',
2801
+ minWidth: 0,
2802
+ opacity: isFadingIn ? 1 : 0,
2803
+ transition: `opacity ${DEFAULT_TRANSITION_DURATION_MS}ms ease`,
2804
+ }),
2805
+ label: {
2806
+ flex: 1,
2807
+ overflow: 'hidden',
2808
+ textOverflow: 'ellipsis',
2809
+ fontWeight: theme.fontWeight.medium,
2810
+ },
2811
+ endContent: {
2812
+ display: 'flex',
2813
+ alignItems: 'center',
2814
+ flexShrink: 0,
2815
+ marginLeft: 'auto',
2816
+ },
2817
+ }));
2818
+
2819
+ /**
2820
+ * Navigation/action item for use inside Drawer.Body or Drawer.Footer.
2821
+ *
2822
+ * - In **expanded** mode: shows icon + label (fade-in) + optional end content.
2823
+ * - In **collapsed** mode: shows only the icon; the label fades out before unmounting
2824
+ * and appears as a right-side tooltip on hover.
2825
+ * - Renders as `<a>` when `href` is provided, otherwise as `<button>`.
2826
+ * - The `selected` prop applies a primary-color highlight.
2827
+ */
2828
+ const DrawerItem = ({ startIcon, label, selected = false, endContent, href, onClick, disabled = false, ariaLabel, ariaLabelledBy, ariaDescribedBy, ariaControls, ariaExpanded, ariaHasPopup, ariaCurrent, }) => {
2829
+ const { isExpanded } = useDrawerContext();
2830
+ const { isVisible: isLabelVisible, isFadingIn: isLabelFadingIn } = useTransitionRender(isExpanded);
2831
+ const rootClassName = DRAWER_ITEM_STYLES.root({ selected, isExpanded });
2832
+ const computedAriaLabel = ariaLabel ?? (!isExpanded ? label : undefined);
2833
+ const computedAriaCurrent = ariaCurrent ?? (selected ? 'page' : undefined);
2834
+ const handleClick = (e) => {
2835
+ if (disabled) {
2836
+ e.preventDefault();
2837
+ return;
2838
+ }
2839
+ onClick?.();
2840
+ };
2841
+ 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 }))] }))] }));
2842
+ 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 }));
2843
+ return (jsxRuntime.jsx(Tooltip, { label: label, placement: 'right', inline: true, withArrow: true, disabled: isExpanded || disabled, children: item }));
2844
+ };
2845
+ DrawerItem.displayName = 'DrawerItem';
2846
+
2847
+ /**
2848
+ * Internal component: renders the temporary drawer as a fixed portal overlay
2849
+ * that slides in from the left with a backdrop, animated via `useTransitionRender`.
2850
+ */
2851
+ const DrawerTemporaryPanel = ({ isExpanded, onClose, children, role, ariaLabel, ariaLabelledBy, ariaDescribedBy, }) => {
2852
+ const { isVisible, isFadingIn } = useTransitionRender(isExpanded);
2853
+ useBodyScrollLock(isExpanded);
2854
+ useKeyPress({ Escape: onClose }, { enabled: isExpanded });
2855
+ if (!isVisible) {
2856
+ return null;
2857
+ }
2858
+ 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);
2859
+ };
2860
+ DrawerTemporaryPanel.displayName = 'DrawerTemporaryPanel';
2861
+ // ─── Main Drawer ─────────────────────────────────────────────────────────────
2862
+ /**
2863
+ * Collapsible side navigation drawer with controlled expanded/collapsed state.
2864
+ *
2865
+ * **Variants**
2866
+ * - `'permanent'` (default on desktop): inline drawer that pushes page content.
2867
+ * Toggles between `expanded` and `collapsed` states with animated width.
2868
+ * - `'temporary'` (default on mobile): portal overlay with a backdrop that slides
2869
+ * in from the left. `isExpanded` controls open/closed; `onClose` is called on
2870
+ * backdrop click or Escape.
2871
+ * - Omit `variant` to auto-detect based on viewport width.
2872
+ *
2873
+ * @example Permanent
2874
+ * <Drawer isExpanded={open} onExpandedChange={setOpen} height="100dvh">
2875
+ * <Drawer.Header>…</Drawer.Header>
2876
+ * <Drawer.Body>
2877
+ * <Drawer.Item startIcon={HomeIcon} label="Home" selected />
2878
+ * </Drawer.Body>
2879
+ * </Drawer>
2880
+ *
2881
+ * @example Temporary
2882
+ * <Drawer variant="temporary" isExpanded={open} onClose={() => setOpen(false)}>
2883
+ * …
2884
+ * </Drawer>
2885
+ */
2886
+ const DrawerBase = ({ height = '100dvh', isExpanded, onExpandedChange, onClose, variant, children, role = 'navigation', ariaLabel = 'Navigation', ariaLabelledBy, ariaDescribedBy, }) => {
2887
+ const resolvedVariant = useDrawerVariant(variant);
2888
+ const handleClose = onClose ?? (() => onExpandedChange?.(false));
2889
+ if (resolvedVariant === 'temporary') {
2890
+ return (jsxRuntime.jsx(DrawerTemporaryPanel, { isExpanded: isExpanded, onClose: handleClose, height: height, role: role, ariaLabel: ariaLabel, ariaLabelledBy: ariaLabelledBy, ariaDescribedBy: ariaDescribedBy, children: children }));
2891
+ }
2892
+ 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 }) }));
2893
+ };
2894
+ DrawerBase.displayName = 'Drawer';
2895
+ const Drawer = DrawerBase;
2896
+ Drawer.Header = DrawerHeader;
2897
+ Drawer.Body = DrawerBody;
2898
+ Drawer.Footer = DrawerFooter;
2899
+ Drawer.Item = DrawerItem;
2900
+
2363
2901
  const AlertContext = React.createContext({
2364
2902
  variant: 'default',
2365
2903
  accentColor: 'defaultActive',
@@ -2431,31 +2969,8 @@ const Alert = AlertBase;
2431
2969
  Alert.Title = AlertTitle;
2432
2970
  Alert.Body = AlertBody;
2433
2971
 
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
2972
  const TRANSITION = `${DEFAULT_TRANSITION_DURATION_MS}ms ease`;
2448
2973
  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
2974
  panel: {
2460
2975
  position: 'fixed',
2461
2976
  inset: 0,
@@ -2513,84 +3028,6 @@ const DialogContext = React.createContext({
2513
3028
  CloseIconComponent: null,
2514
3029
  });
2515
3030
 
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
3031
  const FOCUSABLE_SELECTOR = [
2595
3032
  'a[href]',
2596
3033
  'button:not([disabled])',
@@ -2726,7 +3163,7 @@ const DialogBase = ({ open, onClose, children, closeOnBackdropClick = false, ful
2726
3163
  if (!isVisible) {
2727
3164
  return null;
2728
3165
  }
2729
- return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: theme.cx(DIALOG_STYLES.backdrop, isFadingIn && DIALOG_STYLES.backdropVisible), onClick: handleBackdropClick, "aria-hidden": true }), 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);
3166
+ 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
3167
  };
2731
3168
  DialogBase.displayName = 'Dialog';
2732
3169
  const Dialog = DialogBase;
@@ -2937,13 +3374,19 @@ const themeShadows = {
2937
3374
 
2938
3375
  /**
2939
3376
  * Default spacing tokens
3377
+ *
3378
+ * ⚠️ Token keys MUST stay CSS-custom-property safe (letters, digits, hyphens only).
3379
+ * They are turned into CSS variables (e.g. `smPlus` → `--theme-spacing-sm-plus`) by
3380
+ * the theme proxy, so characters like `+` would produce invalid variable names and
3381
+ * silently break any style that uses them.
2940
3382
  */
2941
3383
  const themeSpacing = {
2942
3384
  none: '0',
2943
3385
  '2xs': '0.125rem', // 2px
2944
3386
  xs: '0.25rem', // 4px
2945
- 'xs+': '0.375rem', // 6px
3387
+ xsPlus: '0.375rem', // 6px
2946
3388
  sm: '0.5rem', // 8px
3389
+ smPlus: '0.75rem', // 12px
2947
3390
  md: '1rem', // 16px
2948
3391
  lg: '1.5rem', // 24px
2949
3392
  xl: '2rem', // 32px
@@ -2957,9 +3400,9 @@ const themeSpacing = {
2957
3400
  * Default transition tokens
2958
3401
  */
2959
3402
  const themeTransition = {
2960
- fast: '150ms ease-out',
2961
- normal: '250ms ease-out',
2962
- slow: '350ms ease-out',
3403
+ fast: '150ms ease-in-out',
3404
+ normal: '250ms ease-in-out',
3405
+ slow: '350ms ease-in-out',
2963
3406
  };
2964
3407
 
2965
3408
  /**
@@ -3115,11 +3558,14 @@ const darkTheme = theme.createTheme({
3115
3558
  });
3116
3559
 
3117
3560
  exports.Alert = Alert;
3561
+ exports.Backdrop = Backdrop;
3118
3562
  exports.Badge = Badge;
3119
3563
  exports.Box = Box;
3120
3564
  exports.Button = Button;
3121
3565
  exports.Card = Card;
3566
+ exports.Checkbox = Checkbox;
3122
3567
  exports.Dialog = Dialog;
3568
+ exports.Drawer = Drawer;
3123
3569
  exports.Form = Form;
3124
3570
  exports.Grid = Grid;
3125
3571
  exports.Icon = Icon;
@@ -3136,4 +3582,5 @@ exports.TextField = TextField;
3136
3582
  exports.Tooltip = Tooltip;
3137
3583
  exports.darkTheme = darkTheme;
3138
3584
  exports.lightTheme = lightTheme;
3585
+ exports.useDrawerContext = useDrawerContext;
3139
3586
  //# sourceMappingURL=index.js.map