@shohojdhara/atomix 0.2.7 → 0.2.9

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +40 -1
  3. package/dist/atomix.css +412 -77
  4. package/dist/atomix.min.css +3 -3
  5. package/dist/index.d.ts +913 -12
  6. package/dist/index.esm.js +1739 -209
  7. package/dist/index.esm.js.map +1 -1
  8. package/dist/index.js +1763 -208
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.min.js +1 -1
  11. package/dist/index.min.js.map +1 -1
  12. package/dist/themes/applemix.css +412 -77
  13. package/dist/themes/applemix.min.css +3 -3
  14. package/dist/themes/boomdevs.css +411 -76
  15. package/dist/themes/boomdevs.min.css +3 -3
  16. package/dist/themes/esrar.css +412 -77
  17. package/dist/themes/esrar.min.css +3 -3
  18. package/dist/themes/flashtrade.css +1803 -622
  19. package/dist/themes/flashtrade.min.css +113 -7
  20. package/dist/themes/mashroom.css +411 -76
  21. package/dist/themes/mashroom.min.css +4 -4
  22. package/dist/themes/shaj-default.css +411 -76
  23. package/dist/themes/shaj-default.min.css +3 -3
  24. package/package.json +13 -2
  25. package/src/components/Button/Button.stories.tsx +174 -0
  26. package/src/components/Button/Button.tsx +238 -78
  27. package/src/components/Card/Card.stories.tsx +202 -0
  28. package/src/components/Card/Card.tsx +253 -77
  29. package/src/components/Form/Input.stories.tsx +228 -2
  30. package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +301 -13
  31. package/src/components/Navigation/SideMenu/SideMenu.tsx +236 -9
  32. package/src/components/Tooltip/Tooltip.tsx +68 -66
  33. package/src/lib/composables/useButton.ts +37 -5
  34. package/src/lib/composables/useInput.ts +39 -1
  35. package/src/lib/composables/useSideMenu.ts +89 -30
  36. package/src/lib/constants/components.ts +53 -0
  37. package/src/lib/index.ts +5 -0
  38. package/src/lib/theme/ThemeContext.tsx +17 -0
  39. package/src/lib/theme/ThemeManager.stories.tsx +472 -0
  40. package/src/lib/theme/ThemeManager.test.ts +186 -0
  41. package/src/lib/theme/ThemeManager.ts +501 -0
  42. package/src/lib/theme/ThemeProvider.tsx +227 -0
  43. package/src/lib/theme/index.ts +56 -0
  44. package/src/lib/theme/types.ts +247 -0
  45. package/src/lib/theme/useTheme.test.tsx +66 -0
  46. package/src/lib/theme/useTheme.ts +80 -0
  47. package/src/lib/theme/utils.test.ts +140 -0
  48. package/src/lib/theme/utils.ts +398 -0
  49. package/src/lib/types/components.ts +304 -4
  50. package/src/styles/01-settings/_settings.tooltip.scss +2 -2
  51. package/src/styles/06-components/_components.button.scss +100 -0
  52. package/src/styles/06-components/_components.card.scss +235 -2
  53. package/src/styles/06-components/_components.side-menu.scss +79 -18
  54. package/src/styles/06-components/_components.tooltip.scss +89 -66
package/dist/index.esm.js CHANGED
@@ -37,7 +37,17 @@ const CLASS_PREFIX = {
37
37
  const BUTTON = {
38
38
  BASE_CLASS: 'c-btn',
39
39
  ICON_CLASS: 'c-btn__icon',
40
+ LABEL_CLASS: 'c-btn__label',
41
+ SPINNER_CLASS: 'c-btn__spinner',
40
42
  VARIANT_PREFIX: 'c-btn--',
43
+ CLASSES: {
44
+ BASE: 'c-btn',
45
+ LOADING: 'c-btn--loading',
46
+ FULL_WIDTH: 'c-btn--full-width',
47
+ BLOCK: 'c-btn--block',
48
+ ACTIVE: 'c-btn--active',
49
+ SELECTED: 'c-btn--selected',
50
+ },
41
51
  };
42
52
  /**
43
53
  * Callout-specific constants
@@ -702,6 +712,22 @@ const INPUT = {
702
712
  INVALID: 'is-invalid',
703
713
  VALID: 'is-valid',
704
714
  DISABLED: 'is-disabled',
715
+ FULL_WIDTH: 'c-input--full-width',
716
+ PREFIX_ICON: 'c-input--prefix-icon',
717
+ SUFFIX_ICON: 'c-input--suffix-icon',
718
+ CLEARABLE: 'c-input--clearable',
719
+ WITH_COUNTER: 'c-input--with-counter',
720
+ PASSWORD_TOGGLE: 'c-input--password-toggle',
721
+ },
722
+ ELEMENTS: {
723
+ WRAPPER: 'c-input-wrapper',
724
+ PREFIX: 'c-input__prefix',
725
+ SUFFIX: 'c-input__suffix',
726
+ CLEAR_BUTTON: 'c-input__clear',
727
+ PASSWORD_TOGGLE: 'c-input__password-toggle',
728
+ COUNTER: 'c-input__counter',
729
+ ERROR_MESSAGE: 'c-input__error',
730
+ HELPER_TEXT: 'c-input__helper',
705
731
  },
706
732
  };
707
733
  /**
@@ -741,15 +767,42 @@ const CARD = {
741
767
  },
742
768
  CLASSES: {
743
769
  BASE: 'c-card',
770
+ // Size modifiers
771
+ SM: 'c-card--sm',
772
+ MD: 'c-card--md',
773
+ LG: 'c-card--lg',
774
+ // Layout modifiers
744
775
  ROW: 'c-card--row',
745
776
  FLAT: 'c-card--flat',
777
+ // Appearance modifiers
778
+ FILLED: 'c-card--filled',
779
+ OUTLINED: 'c-card--outlined',
780
+ GHOST: 'c-card--ghost',
781
+ ELEVATED: 'c-card--elevated',
782
+ // Elevation modifiers
783
+ ELEVATION_NONE: 'c-card--elevation-none',
784
+ ELEVATION_SM: 'c-card--elevation-sm',
785
+ ELEVATION_MD: 'c-card--elevation-md',
786
+ ELEVATION_LG: 'c-card--elevation-lg',
787
+ ELEVATION_XL: 'c-card--elevation-xl',
788
+ // State modifiers
746
789
  ACTIVE: 'is-active',
790
+ DISABLED: 'c-card--disabled',
791
+ LOADING: 'c-card--loading',
792
+ SELECTED: 'c-card--selected',
793
+ INTERACTIVE: 'c-card--interactive',
794
+ // Other modifiers
747
795
  FLIPPED: 'is-flipped',
748
796
  FOCUSED: 'is-focused',
749
797
  CLICKABLE: 'is-clickable',
798
+ GLASS: 'c-card--glass',
750
799
  },
751
800
  DEFAULTS: {
752
801
  HOVER: true,
802
+ SIZE: 'md',
803
+ VARIANT: 'primary',
804
+ APPEARANCE: 'filled',
805
+ ELEVATION: 'none',
753
806
  },
754
807
  };
755
808
  /**
@@ -4253,6 +4306,11 @@ function useButton(initialProps) {
4253
4306
  size: 'md',
4254
4307
  disabled: false,
4255
4308
  rounded: false,
4309
+ loading: false,
4310
+ fullWidth: false,
4311
+ block: false,
4312
+ active: false,
4313
+ selected: false,
4256
4314
  ...initialProps,
4257
4315
  };
4258
4316
  /**
@@ -4261,13 +4319,34 @@ function useButton(initialProps) {
4261
4319
  * @returns Class string
4262
4320
  */
4263
4321
  const generateButtonClass = (props) => {
4264
- const { variant = defaultProps.variant, size = defaultProps.size, disabled = defaultProps.disabled, rounded = defaultProps.rounded, iconOnly = false, glass = defaultProps.glass, className = '', } = props;
4322
+ const { variant = defaultProps.variant, size = defaultProps.size, disabled = defaultProps.disabled, rounded = defaultProps.rounded, iconOnly = false, glass = defaultProps.glass, loading = defaultProps.loading, fullWidth = defaultProps.fullWidth, block = defaultProps.block, active = defaultProps.active, selected = defaultProps.selected, className = '', } = props;
4265
4323
  const sizeClass = size === 'md' ? '' : `c-btn--${size}`;
4266
4324
  const iconOnlyClass = iconOnly ? 'c-btn--icon' : '';
4267
4325
  const roundedClass = rounded ? 'c-btn--rounded' : '';
4268
4326
  const disabledClass = disabled ? 'c-btn--disabled' : '';
4269
4327
  const glassClass = glass ? 'c-btn--glass' : '';
4270
- return `c-btn c-btn--${variant} ${sizeClass} ${iconOnlyClass} ${roundedClass} ${disabledClass} ${glassClass} ${className}`.trim();
4328
+ const loadingClass = loading ? BUTTON.CLASSES.LOADING : '';
4329
+ const fullWidthClass = fullWidth ? BUTTON.CLASSES.FULL_WIDTH : '';
4330
+ const blockClass = block ? BUTTON.CLASSES.BLOCK : '';
4331
+ const activeClass = active ? BUTTON.CLASSES.ACTIVE : '';
4332
+ const selectedClass = selected ? BUTTON.CLASSES.SELECTED : '';
4333
+ return [
4334
+ BUTTON.BASE_CLASS,
4335
+ `c-btn--${variant}`,
4336
+ sizeClass,
4337
+ iconOnlyClass,
4338
+ roundedClass,
4339
+ disabledClass,
4340
+ glassClass,
4341
+ loadingClass,
4342
+ fullWidthClass,
4343
+ blockClass,
4344
+ activeClass,
4345
+ selectedClass,
4346
+ className,
4347
+ ]
4348
+ .filter(Boolean)
4349
+ .join(' ');
4271
4350
  };
4272
4351
  /**
4273
4352
  * Handle button click with disabled check
@@ -4275,9 +4354,9 @@ function useButton(initialProps) {
4275
4354
  * @returns Function that respects disabled state
4276
4355
  */
4277
4356
  const handleClick = (handler) => {
4278
- return () => {
4279
- if (!defaultProps.disabled && handler) {
4280
- handler();
4357
+ return (event) => {
4358
+ if (!defaultProps.disabled && !defaultProps.loading && handler) {
4359
+ handler(event);
4281
4360
  }
4282
4361
  };
4283
4362
  };
@@ -4288,36 +4367,188 @@ function useButton(initialProps) {
4288
4367
  };
4289
4368
  }
4290
4369
 
4291
- const Button = forwardRef(({ label, children, onClick, variant = 'primary', size = 'md', disabled = false, icon, iconOnly = false, rounded = false, className = '', as: Component = 'button', glass, style, ...props }, ref) => {
4370
+ /**
4371
+ * Spinner state and functionality
4372
+ * @param initialProps - Initial spinner properties
4373
+ * @returns Spinner state and methods
4374
+ */
4375
+ function useSpinner(initialProps) {
4376
+ // Default spinner properties
4377
+ const defaultProps = {
4378
+ variant: 'primary',
4379
+ size: 'md',
4380
+ fullscreen: false,
4381
+ ...initialProps,
4382
+ };
4383
+ /**
4384
+ * Generate spinner class based on properties
4385
+ * @param props - Spinner properties
4386
+ * @returns Class string
4387
+ */
4388
+ const generateSpinnerClass = (props) => {
4389
+ const { variant = defaultProps.variant, size = defaultProps.size, fullscreen = defaultProps.fullscreen, className = '', } = props;
4390
+ const baseClass = 'c-spinner';
4391
+ const variantClass = variant ? `${baseClass}--${variant}` : '';
4392
+ const sizeClass = size !== 'md' ? `${baseClass}--${size}` : '';
4393
+ const fullscreenClass = fullscreen ? `${baseClass}--fullscreen` : '';
4394
+ return `${baseClass} ${variantClass} ${sizeClass} ${fullscreenClass} ${className}`.trim();
4395
+ };
4396
+ return {
4397
+ defaultProps,
4398
+ generateSpinnerClass,
4399
+ };
4400
+ }
4401
+
4402
+ const Spinner = ({ size = 'md', variant = 'primary', fullscreen = false, className = '', style, glass, }) => {
4403
+ const { generateSpinnerClass } = useSpinner({
4404
+ size,
4405
+ variant,
4406
+ fullscreen,
4407
+ });
4408
+ const spinnerClass = generateSpinnerClass({
4409
+ size,
4410
+ variant,
4411
+ fullscreen,
4412
+ className: `${className} ${glass ? 'c-spinner--glass' : ''}`.trim(),
4413
+ });
4414
+ const spinnerContent = (jsx("div", { className: spinnerClass, style: style, role: "status", children: jsx("span", { className: SPINNER.VISUALLY_HIDDEN, children: "Loading..." }) }));
4415
+ if (glass) {
4416
+ const defaultGlassProps = {
4417
+ displacementScale: 20,
4418
+ blurAmount: 1,
4419
+ cornerRadius: 999,
4420
+ mode: 'shader',
4421
+ };
4422
+ const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
4423
+ return jsx(AtomixGlass, { ...glassProps, children: spinnerContent });
4424
+ }
4425
+ return spinnerContent;
4426
+ };
4427
+ Spinner.displayName = 'Spinner';
4428
+
4429
+ const Button = React.memo(forwardRef(({ label, children, onClick, variant = 'primary', size = 'md', disabled = false, loading = false, loadingText, icon, iconPosition = 'start', iconOnly = false, rounded = false, fullWidth = false, block = false, active = false, selected = false, type = 'button', className = '', as: Component = 'button', glass, onHover, onFocus, onBlur, ariaLabel, ariaDescribedBy, ariaExpanded, ariaControls, tabIndex, style, ...props }, ref) => {
4430
+ const isDisabled = disabled || loading;
4292
4431
  const { generateButtonClass, handleClick } = useButton({
4293
4432
  variant,
4294
4433
  size,
4295
- disabled,
4434
+ disabled: isDisabled,
4296
4435
  rounded,
4297
4436
  glass,
4437
+ loading,
4438
+ fullWidth,
4439
+ block,
4440
+ active,
4441
+ selected,
4298
4442
  });
4299
- const buttonClass = generateButtonClass({
4443
+ const buttonClass = useMemo(() => generateButtonClass({
4300
4444
  variant,
4301
4445
  size,
4302
- disabled,
4446
+ disabled: isDisabled,
4303
4447
  rounded,
4304
4448
  iconOnly,
4305
4449
  glass,
4450
+ loading,
4451
+ fullWidth,
4452
+ block,
4453
+ active,
4454
+ selected,
4306
4455
  className,
4307
- });
4308
- // Custom styles for glass effect
4309
- const glassStyles = glass ? {} : {};
4310
- // Handle the case when the button is rendered as a link or another component
4311
- const buttonProps = {
4456
+ }), [variant, size, isDisabled, rounded, iconOnly, glass, loading, fullWidth, block, active, selected, className, generateButtonClass]);
4457
+ // Handle click with loading check
4458
+ const handleClickEvent = useCallback((event) => {
4459
+ if (isDisabled) {
4460
+ event.preventDefault();
4461
+ return;
4462
+ }
4463
+ onClick?.(event);
4464
+ }, [isDisabled, onClick]);
4465
+ // Handle hover
4466
+ const handleMouseEnter = useCallback((event) => {
4467
+ if (!isDisabled) {
4468
+ onHover?.(event);
4469
+ }
4470
+ }, [isDisabled, onHover]);
4471
+ // Handle focus
4472
+ const handleFocusEvent = useCallback((event) => {
4473
+ if (!isDisabled) {
4474
+ onFocus?.(event);
4475
+ }
4476
+ }, [isDisabled, onFocus]);
4477
+ // Handle blur
4478
+ const handleBlurEvent = useCallback((event) => {
4479
+ if (!isDisabled) {
4480
+ onBlur?.(event);
4481
+ }
4482
+ }, [isDisabled, onBlur]);
4483
+ // Determine button text
4484
+ const buttonText = useMemo(() => {
4485
+ if (loading && loadingText)
4486
+ return loadingText;
4487
+ if (loading && !loadingText)
4488
+ return label || children;
4489
+ return label || children;
4490
+ }, [loading, loadingText, label, children]);
4491
+ // Determine spinner size based on button size
4492
+ const spinnerSize = useMemo(() => {
4493
+ if (size === 'sm')
4494
+ return 'sm';
4495
+ if (size === 'lg')
4496
+ return 'md';
4497
+ return 'sm';
4498
+ }, [size]);
4499
+ // Button content with icon positioning
4500
+ const buttonContent = useMemo(() => {
4501
+ const iconElement = icon && !loading && (jsx("span", { className: BUTTON.ICON_CLASS, "aria-hidden": "true", children: icon }));
4502
+ const spinnerElement = loading && (jsx("span", { className: BUTTON.SPINNER_CLASS, "aria-hidden": "true", children: jsx(Spinner, { size: spinnerSize, variant: variant === 'link' || (typeof variant === 'string' && variant.startsWith('outline-'))
4503
+ ? 'primary'
4504
+ : (variant === 'danger' ? 'error' : variant) }) }));
4505
+ const labelElement = !iconOnly && buttonText && (jsx("span", { className: BUTTON.LABEL_CLASS, children: buttonText }));
4506
+ if (iconPosition === 'end') {
4507
+ return (jsxs(Fragment, { children: [labelElement, spinnerElement, iconElement] }));
4508
+ }
4509
+ return (jsxs(Fragment, { children: [spinnerElement, iconElement, labelElement] }));
4510
+ }, [icon, iconPosition, iconOnly, buttonText, loading, spinnerSize, variant]);
4511
+ // Button props
4512
+ const buttonProps = useMemo(() => ({
4312
4513
  ref,
4313
4514
  className: buttonClass,
4314
- onClick: handleClick(onClick),
4315
- disabled,
4316
- 'aria-disabled': disabled,
4317
- style: glass ? { ...glassStyles, ...style } : style,
4515
+ type: Component === 'button' ? type : undefined,
4516
+ onClick: handleClickEvent,
4517
+ onMouseEnter: onHover ? handleMouseEnter : undefined,
4518
+ onFocus: onFocus ? handleFocusEvent : undefined,
4519
+ onBlur: onBlur ? handleBlurEvent : undefined,
4520
+ disabled: isDisabled && Component === 'button',
4521
+ 'aria-disabled': isDisabled,
4522
+ 'aria-busy': loading,
4523
+ 'aria-label': ariaLabel || (iconOnly ? label || children : undefined),
4524
+ 'aria-describedby': ariaDescribedBy,
4525
+ 'aria-expanded': ariaExpanded,
4526
+ 'aria-controls': ariaControls,
4527
+ tabIndex: tabIndex !== undefined ? tabIndex : (isDisabled ? -1 : 0),
4528
+ style,
4318
4529
  ...props,
4319
- };
4320
- const buttonContent = (jsxs(Fragment, { children: [icon && jsx("span", { className: "c-btn__icon", children: icon }), !iconOnly && jsx("span", { className: "c-btn__label", children: label || children })] }));
4530
+ }), [
4531
+ ref,
4532
+ buttonClass,
4533
+ Component,
4534
+ type,
4535
+ handleClickEvent,
4536
+ handleMouseEnter,
4537
+ handleFocusEvent,
4538
+ handleBlurEvent,
4539
+ isDisabled,
4540
+ loading,
4541
+ ariaLabel,
4542
+ iconOnly,
4543
+ label,
4544
+ children,
4545
+ ariaDescribedBy,
4546
+ ariaExpanded,
4547
+ ariaControls,
4548
+ tabIndex,
4549
+ style,
4550
+ props,
4551
+ ]);
4321
4552
  if (glass) {
4322
4553
  // Default glass settings for buttons
4323
4554
  const defaultGlassProps = {
@@ -4330,7 +4561,7 @@ const Button = forwardRef(({ label, children, onClick, variant = 'primary', size
4330
4561
  return (jsx(AtomixGlass, { ...glassProps, children: jsx(Component, { ...buttonProps, children: buttonContent }) }));
4331
4562
  }
4332
4563
  return jsx(Component, { ...buttonProps, children: buttonContent });
4333
- });
4564
+ }));
4334
4565
  Button.displayName = 'Button';
4335
4566
 
4336
4567
  /**
@@ -4426,23 +4657,150 @@ const Callout = ({ title, children, icon, variant = 'primary', onClose, actions,
4426
4657
  };
4427
4658
  Callout.displayName = 'Callout';
4428
4659
 
4429
- const Card = forwardRef(({ header, image, imageAlt = '', title, text, actions, icon, footer, row = false, flat = false, active = false, className = '', children, onClick, style, glass, ...rest }, ref) => {
4430
- const cardClasses = [
4660
+ const Card = React.memo(forwardRef(({
4661
+ // Variants
4662
+ size = 'md', variant = '', appearance = 'filled', elevation = 'none',
4663
+ // Layout
4664
+ row = false, flat = false,
4665
+ // States
4666
+ active = false, disabled = false, loading = false, selected = false, interactive = false,
4667
+ // Content
4668
+ header, image, imageAlt = '', title, text, actions, icon, footer, children,
4669
+ // Interaction
4670
+ onClick, onHover, onFocus, href, target,
4671
+ // Glass
4672
+ glass,
4673
+ // Accessibility
4674
+ role, ariaLabel, ariaDescribedBy, tabIndex,
4675
+ // Styling
4676
+ className = '', style, ...rest }, ref) => {
4677
+ // Determine if card is clickable/interactive
4678
+ const isClickable = Boolean(onClick || href || interactive);
4679
+ const isDisabled = disabled || loading;
4680
+ // Build CSS classes using BEM methodology
4681
+ const cardClasses = useMemo(() => [
4431
4682
  CARD.CLASSES.BASE,
4683
+ // Size modifiers
4684
+ size === 'sm' ? CARD.CLASSES.SM : '',
4685
+ size === 'md' ? CARD.CLASSES.MD : '',
4686
+ size === 'lg' ? CARD.CLASSES.LG : '',
4687
+ // Variant modifiers (will be handled in SCSS with @each)
4688
+ variant ? `c-card--${variant}` : '',
4689
+ // Appearance modifiers
4690
+ appearance === 'filled' ? CARD.CLASSES.FILLED : '',
4691
+ appearance === 'outlined' ? CARD.CLASSES.OUTLINED : '',
4692
+ appearance === 'ghost' ? CARD.CLASSES.GHOST : '',
4693
+ appearance === 'elevated' ? CARD.CLASSES.ELEVATED : '',
4694
+ // Elevation modifiers
4695
+ elevation === 'none' ? CARD.CLASSES.ELEVATION_NONE : '',
4696
+ elevation === 'sm' ? CARD.CLASSES.ELEVATION_SM : '',
4697
+ elevation === 'md' ? CARD.CLASSES.ELEVATION_MD : '',
4698
+ elevation === 'lg' ? CARD.CLASSES.ELEVATION_LG : '',
4699
+ elevation === 'xl' ? CARD.CLASSES.ELEVATION_XL : '',
4700
+ // Layout modifiers
4432
4701
  row ? CARD.CLASSES.ROW : '',
4433
4702
  flat ? CARD.CLASSES.FLAT : '',
4703
+ // State modifiers
4434
4704
  active ? CARD.CLASSES.ACTIVE : '',
4705
+ disabled ? CARD.CLASSES.DISABLED : '',
4706
+ loading ? CARD.CLASSES.LOADING : '',
4707
+ selected ? CARD.CLASSES.SELECTED : '',
4708
+ interactive || isClickable ? CARD.CLASSES.INTERACTIVE : '',
4709
+ glass ? CARD.CLASSES.GLASS : '',
4435
4710
  className,
4436
4711
  ]
4437
4712
  .filter(Boolean)
4438
- .join(' ');
4439
- const cardContent = (jsxs(Fragment, { children: [(image || icon || header) && (jsxs("div", { className: CARD.SELECTORS.HEADER.substring(1), children: [header, image && (jsx("img", { src: image, alt: imageAlt, className: CARD.SELECTORS.IMAGE.substring(1) })), icon && jsx("div", { className: CARD.SELECTORS.ICON.substring(1), children: icon })] })), jsxs("div", { className: CARD.SELECTORS.BODY.substring(1), children: [title && jsx("h3", { className: CARD.SELECTORS.TITLE.substring(1), children: title }), text && jsx("p", { className: CARD.SELECTORS.TEXT.substring(1), children: text }), children] }), actions && jsx("div", { className: CARD.SELECTORS.ACTIONS.substring(1), children: actions }), footer && jsx("div", { className: CARD.SELECTORS.FOOTER.substring(1), children: footer })] }));
4713
+ .join(' '), [size, variant, appearance, elevation, row, flat, active, disabled, loading, selected, interactive, isClickable, glass, className]);
4714
+ // Determine ARIA role
4715
+ const cardRole = useMemo(() => {
4716
+ if (role)
4717
+ return role;
4718
+ if (href)
4719
+ return 'link';
4720
+ if (isClickable)
4721
+ return 'button';
4722
+ return 'article';
4723
+ }, [role, href, isClickable]);
4724
+ // Handle click events
4725
+ const handleClick = useCallback((event) => {
4726
+ if (isDisabled) {
4727
+ event.preventDefault();
4728
+ return;
4729
+ }
4730
+ onClick?.(event);
4731
+ }, [isDisabled, onClick]);
4732
+ // Handle keyboard events for accessibility
4733
+ const handleKeyDown = useCallback((event) => {
4734
+ if (isDisabled) {
4735
+ event.preventDefault();
4736
+ return;
4737
+ }
4738
+ // Enter or Space activates clickable cards
4739
+ if (isClickable && (event.key === 'Enter' || event.key === ' ')) {
4740
+ event.preventDefault();
4741
+ if (onClick) {
4742
+ onClick(event);
4743
+ }
4744
+ // If href is provided, the anchor will handle navigation
4745
+ }
4746
+ }, [isDisabled, isClickable, onClick]);
4747
+ // Handle hover events
4748
+ const handleMouseEnter = useCallback((event) => {
4749
+ if (!isDisabled) {
4750
+ onHover?.(event);
4751
+ }
4752
+ }, [isDisabled, onHover]);
4753
+ // Handle focus events
4754
+ const handleFocusEvent = useCallback((event) => {
4755
+ if (!isDisabled) {
4756
+ onFocus?.(event);
4757
+ }
4758
+ }, [isDisabled, onFocus]);
4759
+ // Determine tab index
4760
+ const effectiveTabIndex = useMemo(() => {
4761
+ if (tabIndex !== undefined)
4762
+ return tabIndex;
4763
+ if (isDisabled)
4764
+ return -1;
4765
+ if (isClickable)
4766
+ return 0;
4767
+ return undefined;
4768
+ }, [tabIndex, isDisabled, isClickable]);
4769
+ // Card content structure
4770
+ const cardContent = useMemo(() => (jsxs(Fragment, { children: [loading && (jsx("div", { className: "c-card__loading", "aria-label": "Loading", children: jsx("div", { className: "c-card__spinner" }) })), (image || icon || header) && (jsxs("div", { className: CARD.SELECTORS.HEADER.substring(1), children: [header, image && (jsx("img", { src: image, alt: imageAlt, className: CARD.SELECTORS.IMAGE.substring(1), loading: "lazy" })), icon && jsx("div", { className: CARD.SELECTORS.ICON.substring(1), children: icon })] })), jsxs("div", { className: CARD.SELECTORS.BODY.substring(1), children: [title && jsx("h3", { className: CARD.SELECTORS.TITLE.substring(1), children: title }), text && jsx("p", { className: CARD.SELECTORS.TEXT.substring(1), children: text }), children] }), actions && jsx("div", { className: CARD.SELECTORS.ACTIONS.substring(1), children: actions }), footer && jsx("div", { className: CARD.SELECTORS.FOOTER.substring(1), children: footer })] })), [loading, image, imageAlt, icon, header, title, text, children, actions, footer]);
4771
+ // Common props for both div and anchor
4772
+ const commonProps = {
4773
+ // ref is applied individually to ensure correct typing for polymorphic behavior
4774
+ className: cardClasses,
4775
+ style,
4776
+ role: cardRole,
4777
+ 'aria-label': ariaLabel,
4778
+ 'aria-describedby': ariaDescribedBy,
4779
+ 'aria-disabled': isDisabled ? true : undefined,
4780
+ tabIndex: effectiveTabIndex,
4781
+ onClick: isClickable ? handleClick : undefined,
4782
+ onKeyDown: isClickable ? handleKeyDown : undefined,
4783
+ onMouseEnter: onHover ? handleMouseEnter : undefined,
4784
+ onFocus: onFocus ? handleFocusEvent : undefined,
4785
+ ...rest,
4786
+ };
4787
+ // Render as anchor if href is provided
4788
+ if (href && !isDisabled) {
4789
+ const anchorElement = (jsx("a", { ...commonProps, ref: ref, href: href, target: target, rel: target === '_blank' ? 'noopener noreferrer' : undefined, children: cardContent }));
4790
+ if (glass) {
4791
+ const glassProps = glass === true ? {} : glass;
4792
+ return (jsx(AtomixGlass, { ...glassProps, elasticity: 0, children: anchorElement }));
4793
+ }
4794
+ return anchorElement;
4795
+ }
4796
+ // Render as div
4797
+ const divElement = (jsx("div", { ...commonProps, ref: ref, children: cardContent }));
4440
4798
  if (glass) {
4441
4799
  const glassProps = glass === true ? {} : glass;
4442
- return (jsxs(AtomixGlass, { ...glassProps, elasticity: 0, children: [' ', jsx("div", { ref: ref, className: cardClasses + ' c-card--glass', onClick: onClick, ...rest, style: { ...style }, children: cardContent })] }));
4800
+ return (jsx(AtomixGlass, { ...glassProps, elasticity: 0, children: divElement }));
4443
4801
  }
4444
- return (jsx("div", { ref: ref, className: cardClasses, onClick: onClick, ...rest, style: { ...style }, children: cardContent }));
4445
- });
4802
+ return divElement;
4803
+ }));
4446
4804
  Card.displayName = 'Card';
4447
4805
 
4448
4806
  /**
@@ -9247,65 +9605,6 @@ function useDataTable({ data = [], columns = [], sortable = false, paginated = f
9247
9605
  };
9248
9606
  }
9249
9607
 
9250
- /**
9251
- * Spinner state and functionality
9252
- * @param initialProps - Initial spinner properties
9253
- * @returns Spinner state and methods
9254
- */
9255
- function useSpinner(initialProps) {
9256
- // Default spinner properties
9257
- const defaultProps = {
9258
- variant: 'primary',
9259
- size: 'md',
9260
- fullscreen: false,
9261
- ...initialProps,
9262
- };
9263
- /**
9264
- * Generate spinner class based on properties
9265
- * @param props - Spinner properties
9266
- * @returns Class string
9267
- */
9268
- const generateSpinnerClass = (props) => {
9269
- const { variant = defaultProps.variant, size = defaultProps.size, fullscreen = defaultProps.fullscreen, className = '', } = props;
9270
- const baseClass = 'c-spinner';
9271
- const variantClass = variant ? `${baseClass}--${variant}` : '';
9272
- const sizeClass = size !== 'md' ? `${baseClass}--${size}` : '';
9273
- const fullscreenClass = fullscreen ? `${baseClass}--fullscreen` : '';
9274
- return `${baseClass} ${variantClass} ${sizeClass} ${fullscreenClass} ${className}`.trim();
9275
- };
9276
- return {
9277
- defaultProps,
9278
- generateSpinnerClass,
9279
- };
9280
- }
9281
-
9282
- const Spinner = ({ size = 'md', variant = 'primary', fullscreen = false, className = '', style, glass, }) => {
9283
- const { generateSpinnerClass } = useSpinner({
9284
- size,
9285
- variant,
9286
- fullscreen,
9287
- });
9288
- const spinnerClass = generateSpinnerClass({
9289
- size,
9290
- variant,
9291
- fullscreen,
9292
- className: `${className} ${glass ? 'c-spinner--glass' : ''}`.trim(),
9293
- });
9294
- const spinnerContent = (jsx("div", { className: spinnerClass, style: style, role: "status", children: jsx("span", { className: SPINNER.VISUALLY_HIDDEN, children: "Loading..." }) }));
9295
- if (glass) {
9296
- const defaultGlassProps = {
9297
- displacementScale: 20,
9298
- blurAmount: 1,
9299
- cornerRadius: 999,
9300
- mode: 'shader',
9301
- };
9302
- const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
9303
- return jsx(AtomixGlass, { ...glassProps, children: spinnerContent });
9304
- }
9305
- return spinnerContent;
9306
- };
9307
- Spinner.displayName = 'Spinner';
9308
-
9309
9608
  const DOTS = '...';
9310
9609
  const range = (start, end) => {
9311
9610
  const length = end - start + 1;
@@ -10875,9 +11174,35 @@ function useInput(initialProps) {
10875
11174
  const disabledClass = disabled ? INPUT.CLASSES.DISABLED : '';
10876
11175
  return `${INPUT.CLASSES.BASE} ${sizeClass} ${variantClass} ${textareaClass} ${validationClass} ${disabledClass} ${className}`.trim();
10877
11176
  };
11177
+ /**
11178
+ * Generate wrapper class based on properties
11179
+ * @param props - Wrapper properties
11180
+ * @returns Class string
11181
+ */
11182
+ const generateWrapperClass = (props) => {
11183
+ const { className = '' } = props;
11184
+ const { prefixIcon = false, suffixIcon = false, clearable = false, showCounter = false, showPasswordToggle = false, fullWidth = false, } = initialProps || {};
11185
+ const classes = [INPUT.ELEMENTS.WRAPPER];
11186
+ if (prefixIcon)
11187
+ classes.push(INPUT.CLASSES.PREFIX_ICON);
11188
+ if (suffixIcon || clearable || showPasswordToggle)
11189
+ classes.push(INPUT.CLASSES.SUFFIX_ICON);
11190
+ if (clearable)
11191
+ classes.push(INPUT.CLASSES.CLEARABLE);
11192
+ if (showCounter)
11193
+ classes.push(INPUT.CLASSES.WITH_COUNTER);
11194
+ if (showPasswordToggle)
11195
+ classes.push(INPUT.CLASSES.PASSWORD_TOGGLE);
11196
+ if (fullWidth)
11197
+ classes.push(INPUT.CLASSES.FULL_WIDTH);
11198
+ if (className)
11199
+ classes.push(className);
11200
+ return classes.filter(Boolean).join(' ');
11201
+ };
10878
11202
  return {
10879
11203
  defaultProps,
10880
11204
  generateInputClass,
11205
+ generateWrapperClass,
10881
11206
  };
10882
11207
  }
10883
11208
 
@@ -11555,67 +11880,115 @@ function useSideMenu(initialProps) {
11555
11880
  // Default side menu properties
11556
11881
  const defaultProps = {
11557
11882
  collapsible: true,
11883
+ collapsibleDesktop: false,
11884
+ defaultCollapsedDesktop: false,
11558
11885
  isOpen: false,
11559
11886
  ...initialProps,
11560
11887
  };
11561
11888
  // Local open state for when not controlled externally
11562
- const [isOpenState, setIsOpenState] = useState(defaultProps.isOpen || false);
11889
+ const [isOpenState, setIsOpenState] = useState(defaultProps.defaultCollapsedDesktop !== undefined
11890
+ ? !defaultProps.defaultCollapsedDesktop
11891
+ : (defaultProps.isOpen || false));
11563
11892
  // Refs for managing responsive behavior
11564
11893
  const wrapperRef = useRef(null);
11565
11894
  const innerRef = useRef(null);
11895
+ const sideMenuRef = useRef(null);
11566
11896
  // Update local state when external state changes
11567
11897
  useEffect(() => {
11568
11898
  if (typeof defaultProps.isOpen !== 'undefined') {
11569
11899
  setIsOpenState(defaultProps.isOpen);
11570
11900
  }
11571
- }, [defaultProps.isOpen]);
11572
- // Handle responsive behavior - auto-open on desktop, controlled on mobile
11901
+ else if (defaultProps.defaultCollapsedDesktop !== undefined) {
11902
+ setIsOpenState(!defaultProps.defaultCollapsedDesktop);
11903
+ }
11904
+ }, [defaultProps.isOpen, defaultProps.defaultCollapsedDesktop]);
11905
+ // Set initial height on mount
11906
+ useEffect(() => {
11907
+ const isMobile = window.innerWidth < 768;
11908
+ const shouldCollapse = isMobile ? defaultProps.collapsible : defaultProps.collapsibleDesktop;
11909
+ const currentOpen = typeof defaultProps.isOpen !== 'undefined' ? defaultProps.isOpen : isOpenState;
11910
+ if (shouldCollapse && wrapperRef.current && innerRef.current) {
11911
+ // Use setTimeout to ensure DOM is fully rendered
11912
+ const timeoutId = setTimeout(() => {
11913
+ if (wrapperRef.current && innerRef.current) {
11914
+ if (currentOpen) {
11915
+ wrapperRef.current.style.height = `${innerRef.current.scrollHeight}px`;
11916
+ }
11917
+ else {
11918
+ wrapperRef.current.style.height = '0px';
11919
+ }
11920
+ }
11921
+ }, 0);
11922
+ return () => clearTimeout(timeoutId);
11923
+ }
11924
+ else if (!shouldCollapse && wrapperRef.current) {
11925
+ wrapperRef.current.style.height = 'auto';
11926
+ }
11927
+ }, []); // Only run on mount
11928
+ // Handle responsive behavior - vertical collapse for both mobile and desktop
11573
11929
  useEffect(() => {
11574
11930
  const handleResize = () => {
11575
11931
  const isMobile = window.innerWidth < 768; // MD breakpoint
11576
- if (!isMobile && defaultProps.collapsible) {
11577
- // Auto-open on desktop
11578
- if (typeof defaultProps.onToggle === 'function') {
11579
- defaultProps.onToggle(true);
11580
- }
11581
- else {
11582
- setIsOpenState(true);
11583
- }
11584
- // Reset wrapper height on desktop
11932
+ const shouldCollapse = isMobile ? defaultProps.collapsible : defaultProps.collapsibleDesktop;
11933
+ if (!shouldCollapse) {
11934
+ // Not collapsible - always show content
11585
11935
  if (wrapperRef.current) {
11586
11936
  wrapperRef.current.style.height = 'auto';
11587
11937
  }
11588
11938
  }
11589
- else if (isMobile && wrapperRef.current && innerRef.current) {
11590
- // Set proper height for mobile animation
11939
+ else if (wrapperRef.current && innerRef.current) {
11940
+ // Set proper height for vertical animation (both mobile and desktop)
11591
11941
  const currentOpen = typeof defaultProps.isOpen !== 'undefined' ? defaultProps.isOpen : isOpenState;
11592
- if (currentOpen) {
11593
- wrapperRef.current.style.height = `${innerRef.current.scrollHeight}px`;
11594
- }
11595
- else {
11596
- wrapperRef.current.style.height = '0px';
11597
- }
11942
+ // Use requestAnimationFrame to ensure DOM is ready
11943
+ requestAnimationFrame(() => {
11944
+ if (wrapperRef.current && innerRef.current) {
11945
+ if (currentOpen) {
11946
+ wrapperRef.current.style.height = `${innerRef.current.scrollHeight}px`;
11947
+ }
11948
+ else {
11949
+ wrapperRef.current.style.height = '0px';
11950
+ }
11951
+ }
11952
+ });
11598
11953
  }
11599
11954
  };
11600
- handleResize(); // Initial call
11955
+ // Initial call with a small delay to ensure DOM is ready
11956
+ const timeoutId = setTimeout(handleResize, 0);
11601
11957
  window.addEventListener('resize', handleResize);
11602
11958
  return () => {
11959
+ clearTimeout(timeoutId);
11603
11960
  window.removeEventListener('resize', handleResize);
11604
11961
  };
11605
- }, [defaultProps.collapsible, defaultProps.isOpen, defaultProps.onToggle, isOpenState]);
11606
- // Update wrapper height when open state changes on mobile
11962
+ }, [
11963
+ defaultProps.collapsible,
11964
+ defaultProps.collapsibleDesktop,
11965
+ defaultProps.isOpen,
11966
+ defaultProps.onToggle,
11967
+ isOpenState,
11968
+ ]);
11969
+ // Update wrapper height when open state changes (both mobile and desktop)
11607
11970
  useEffect(() => {
11608
11971
  const isMobile = window.innerWidth < 768;
11609
- if (isMobile && wrapperRef.current && innerRef.current && defaultProps.collapsible) {
11972
+ const shouldCollapse = isMobile ? defaultProps.collapsible : defaultProps.collapsibleDesktop;
11973
+ if (shouldCollapse && wrapperRef.current && innerRef.current) {
11610
11974
  const currentOpen = typeof defaultProps.isOpen !== 'undefined' ? defaultProps.isOpen : isOpenState;
11611
- if (currentOpen) {
11612
- wrapperRef.current.style.height = `${innerRef.current.scrollHeight}px`;
11613
- }
11614
- else {
11615
- wrapperRef.current.style.height = '0px';
11616
- }
11975
+ // Use requestAnimationFrame to ensure DOM is ready
11976
+ requestAnimationFrame(() => {
11977
+ if (wrapperRef.current && innerRef.current) {
11978
+ if (currentOpen) {
11979
+ wrapperRef.current.style.height = `${innerRef.current.scrollHeight}px`;
11980
+ }
11981
+ else {
11982
+ wrapperRef.current.style.height = '0px';
11983
+ }
11984
+ }
11985
+ });
11986
+ }
11987
+ else if (!shouldCollapse && wrapperRef.current) {
11988
+ // Not collapsible - always show content
11989
+ wrapperRef.current.style.height = 'auto';
11617
11990
  }
11618
- }, [defaultProps.isOpen, isOpenState, defaultProps.collapsible]);
11991
+ }, [defaultProps.isOpen, isOpenState, defaultProps.collapsible, defaultProps.collapsibleDesktop]);
11619
11992
  /**
11620
11993
  * Generate side menu class based on properties
11621
11994
  * @param props - Side menu properties
@@ -11634,7 +12007,7 @@ function useSideMenu(initialProps) {
11634
12007
  return SIDE_MENU.CLASSES.WRAPPER;
11635
12008
  };
11636
12009
  /**
11637
- * Handle toggle click
12010
+ * Handle toggle click (mobile)
11638
12011
  */
11639
12012
  const handleToggle = () => {
11640
12013
  if (defaultProps.disabled)
@@ -11649,6 +12022,12 @@ function useSideMenu(initialProps) {
11649
12022
  setIsOpenState(newState);
11650
12023
  }
11651
12024
  };
12025
+ /**
12026
+ * Handle desktop collapse toggle (uses same toggle as mobile)
12027
+ */
12028
+ const handleDesktopCollapse = () => {
12029
+ handleToggle();
12030
+ };
11652
12031
  /**
11653
12032
  * Get current open state
11654
12033
  * @returns Current open state
@@ -11661,9 +12040,11 @@ function useSideMenu(initialProps) {
11661
12040
  isOpenState: getCurrentOpenState(),
11662
12041
  wrapperRef,
11663
12042
  innerRef,
12043
+ sideMenuRef,
11664
12044
  generateSideMenuClass,
11665
12045
  generateWrapperClass,
11666
12046
  handleToggle,
12047
+ handleDesktopCollapse,
11667
12048
  getCurrentOpenState,
11668
12049
  };
11669
12050
  }
@@ -14383,50 +14764,27 @@ const Navbar = forwardRef(({ brand, children, variant, position = 'static', cont
14383
14764
  Navbar.displayName = 'Navbar';
14384
14765
 
14385
14766
  /**
14386
- * SideMenu component provides a collapsible navigation menu with title and menu items.
14387
- * Automatically collapses on mobile devices and can be toggled via a header button.
14767
+ * SideMenuList component provides a container for side menu items.
14388
14768
  *
14389
14769
  * @example
14390
14770
  * ```tsx
14391
- * <SideMenu title="Navigation">
14392
- * <SideMenuList>
14393
- * <SideMenuItem href="/" active>Home</SideMenuItem>
14394
- * <SideMenuItem href="/about">About</SideMenuItem>
14395
- * <SideMenuItem href="/contact">Contact</SideMenuItem>
14396
- * </SideMenuList>
14397
- * </SideMenu>
14771
+ * <SideMenuList>
14772
+ * <SideMenuItem href="/" active>Home</SideMenuItem>
14773
+ * <SideMenuItem href="/about">About</SideMenuItem>
14774
+ * <SideMenuItem href="/contact">Contact</SideMenuItem>
14775
+ * </SideMenuList>
14398
14776
  * ```
14399
14777
  */
14400
- const SideMenu = forwardRef(({ title, children, isOpen, onToggle, collapsible = true, className = '', style, disabled = false, toggleIcon, id, glass, }, ref) => {
14401
- const { isOpenState, wrapperRef, innerRef, generateSideMenuClass, generateWrapperClass, handleToggle, } = useSideMenu({
14402
- isOpen,
14403
- onToggle,
14404
- collapsible,
14405
- disabled,
14406
- });
14407
- const sideMenuClass = generateSideMenuClass({ className, isOpen: isOpenState });
14408
- const wrapperClass = generateWrapperClass();
14409
- // Default toggle icon using Atomix Icon component
14410
- const defaultToggleIcon = jsx(Icon, { name: "CaretRight", size: "xs" });
14411
- const sideMenuContent = (jsxs(Fragment, { children: [title && collapsible && (jsxs("div", { className: "c-side-menu__toggler", onClick: handleToggle, role: "button", tabIndex: disabled ? -1 : 0, "aria-expanded": isOpenState, "aria-controls": id ? `${id}-content` : undefined, "aria-disabled": disabled, onKeyDown: e => {
14412
- if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
14413
- e.preventDefault();
14414
- handleToggle();
14415
- }
14416
- }, children: [jsx("span", { className: "c-side-menu__title", children: title }), jsx("span", { className: "c-side-menu__toggler-icon", children: toggleIcon || defaultToggleIcon })] })), title && !collapsible && jsx("h3", { className: "c-side-menu__title", children: title }), jsx("div", { ref: wrapperRef, className: wrapperClass, id: id ? `${id}-content` : undefined, "aria-hidden": collapsible ? !isOpenState : false, children: jsx("div", { ref: innerRef, className: "c-side-menu__inner", children: children }) })] }));
14417
- if (glass) {
14418
- const defaultGlassProps = {
14419
- displacementScale: 70,
14420
- blurAmount: 2,
14421
- cornerRadius: 12,
14422
- mode: 'shader',
14423
- };
14424
- const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
14425
- return (jsx(AtomixGlass, { ...glassProps, children: jsx("div", { ref: ref, className: sideMenuClass + ' c-side-menu--glass', id: id, style: style, children: sideMenuContent }) }));
14426
- }
14427
- return (jsx("div", { ref: ref, className: sideMenuClass, id: id, style: style, children: sideMenuContent }));
14778
+ const SideMenuList = forwardRef(({ children, className = '' }, ref) => {
14779
+ const listClass = `c-side-menu__list ${className}`.trim();
14780
+ return (jsx("ul", { ref: ref, className: listClass, role: "list", children: React.Children.map(children, (child, index) => {
14781
+ if (React.isValidElement(child)) {
14782
+ return (jsx("li", { className: "c-side-menu__item", role: "listitem", children: child }, index));
14783
+ }
14784
+ return child;
14785
+ }) }));
14428
14786
  });
14429
- SideMenu.displayName = 'SideMenu';
14787
+ SideMenuList.displayName = 'SideMenuList';
14430
14788
 
14431
14789
  /**
14432
14790
  * SideMenuItem component represents a single navigation item in a side menu.
@@ -14481,27 +14839,199 @@ const SideMenuItem = forwardRef(({ children, href, onClick, active = false, disa
14481
14839
  SideMenuItem.displayName = 'SideMenuItem';
14482
14840
 
14483
14841
  /**
14484
- * SideMenuList component provides a container for side menu items.
14842
+ * SideMenu component provides a collapsible navigation menu with title and menu items.
14843
+ * Automatically collapses on mobile devices and can be toggled via a header button.
14485
14844
  *
14486
14845
  * @example
14487
14846
  * ```tsx
14488
- * <SideMenuList>
14489
- * <SideMenuItem href="/" active>Home</SideMenuItem>
14490
- * <SideMenuItem href="/about">About</SideMenuItem>
14491
- * <SideMenuItem href="/contact">Contact</SideMenuItem>
14492
- * </SideMenuList>
14847
+ * <SideMenu title="Navigation">
14848
+ * <SideMenuList>
14849
+ * <SideMenuItem href="/" active>Home</SideMenuItem>
14850
+ * <SideMenuItem href="/about">About</SideMenuItem>
14851
+ * <SideMenuItem href="/contact">Contact</SideMenuItem>
14852
+ * </SideMenuList>
14853
+ * </SideMenu>
14493
14854
  * ```
14494
14855
  */
14495
- const SideMenuList = forwardRef(({ children, className = '' }, ref) => {
14496
- const listClass = `c-side-menu__list ${className}`.trim();
14497
- return (jsx("ul", { ref: ref, className: listClass, role: "list", children: React.Children.map(children, (child, index) => {
14498
- if (React.isValidElement(child)) {
14499
- return (jsx("li", { className: "c-side-menu__item", role: "listitem", children: child }, index));
14856
+ const SideMenu = forwardRef(({ title, children, menuItems = [], isOpen, onToggle, collapsible = true, collapsibleDesktop = false, defaultCollapsedDesktop = false, className = '', style, disabled = false, toggleIcon, id, glass, }, ref) => {
14857
+ const { isOpenState, wrapperRef, innerRef, sideMenuRef, generateSideMenuClass, generateWrapperClass, handleToggle, handleDesktopCollapse, } = useSideMenu({
14858
+ isOpen,
14859
+ onToggle,
14860
+ collapsible,
14861
+ collapsibleDesktop,
14862
+ defaultCollapsedDesktop,
14863
+ disabled,
14864
+ });
14865
+ const MOBILE_BREAKPOINT = 768;
14866
+ // Track mobile state
14867
+ const [isMobileState, setIsMobileState] = useState(() => {
14868
+ if (typeof window === 'undefined')
14869
+ return false;
14870
+ return window.innerWidth < MOBILE_BREAKPOINT;
14871
+ });
14872
+ // Track open state for nested menu items
14873
+ const [nestedItemStates, setNestedItemStates] = useState(() => {
14874
+ const initialState = {};
14875
+ menuItems?.forEach((_, index) => {
14876
+ initialState[index] = true; // Default to open
14877
+ });
14878
+ return initialState;
14879
+ });
14880
+ // Refs for nested menu item wrappers
14881
+ const nestedWrapperRefs = useRef({});
14882
+ const nestedInnerRefs = useRef({});
14883
+ const menuItemsLengthRef = useRef(menuItems?.length ?? 0);
14884
+ useEffect(() => {
14885
+ const handleResize = () => {
14886
+ setIsMobileState(window.innerWidth < MOBILE_BREAKPOINT);
14887
+ };
14888
+ window.addEventListener('resize', handleResize);
14889
+ return () => window.removeEventListener('resize', handleResize);
14890
+ }, []);
14891
+ // Update nested item states when menuItems change
14892
+ useEffect(() => {
14893
+ const currentLength = menuItems?.length ?? 0;
14894
+ // Only update if the length actually changed to prevent infinite loops
14895
+ if (menuItemsLengthRef.current === currentLength)
14896
+ return;
14897
+ menuItemsLengthRef.current = currentLength;
14898
+ setNestedItemStates(prevStates => {
14899
+ const newStates = {};
14900
+ menuItems?.forEach((_, index) => {
14901
+ newStates[index] = prevStates[index] ?? true;
14902
+ });
14903
+ return newStates;
14904
+ });
14905
+ // Clean up refs for removed items
14906
+ Object.keys(nestedWrapperRefs.current).forEach(key => {
14907
+ const index = Number(key);
14908
+ if (index >= currentLength) {
14909
+ delete nestedWrapperRefs.current[index];
14910
+ delete nestedInnerRefs.current[index];
14500
14911
  }
14501
- return child;
14502
- }) }));
14912
+ });
14913
+ }, [menuItems?.length]);
14914
+ // Set initial heights for nested wrappers on mount and when menuItems change
14915
+ useEffect(() => {
14916
+ if (!menuItems?.length)
14917
+ return;
14918
+ const timeoutId = setTimeout(() => {
14919
+ menuItems.forEach((_, index) => {
14920
+ const wrapper = nestedWrapperRefs.current[index];
14921
+ const inner = nestedInnerRefs.current[index];
14922
+ const isOpen = nestedItemStates[index] ?? true;
14923
+ if (wrapper && inner) {
14924
+ if (isOpen) {
14925
+ wrapper.style.height = `${inner.scrollHeight}px`;
14926
+ }
14927
+ else {
14928
+ wrapper.style.height = '0px';
14929
+ }
14930
+ }
14931
+ });
14932
+ }, 0);
14933
+ return () => clearTimeout(timeoutId);
14934
+ // Only run when menuItems change, nestedItemStates is read but not in deps to avoid loops
14935
+ // eslint-disable-next-line react-hooks/exhaustive-deps
14936
+ }, [menuItems?.length]);
14937
+ // Update nested wrapper heights when state changes
14938
+ useEffect(() => {
14939
+ if (!menuItems?.length)
14940
+ return;
14941
+ const frameIds = [];
14942
+ Object.keys(nestedItemStates).forEach(key => {
14943
+ const index = Number(key);
14944
+ const wrapper = nestedWrapperRefs.current[index];
14945
+ const inner = nestedInnerRefs.current[index];
14946
+ const isOpen = nestedItemStates[index];
14947
+ if (wrapper && inner) {
14948
+ const frameId = requestAnimationFrame(() => {
14949
+ if (wrapper && inner) {
14950
+ if (isOpen) {
14951
+ wrapper.style.height = `${inner.scrollHeight}px`;
14952
+ }
14953
+ else {
14954
+ wrapper.style.height = '0px';
14955
+ }
14956
+ }
14957
+ });
14958
+ frameIds.push(frameId);
14959
+ }
14960
+ });
14961
+ return () => {
14962
+ frameIds.forEach(id => cancelAnimationFrame(id));
14963
+ };
14964
+ }, [nestedItemStates, menuItems?.length]);
14965
+ // Combine refs
14966
+ const combinedRef = (node) => {
14967
+ sideMenuRef.current = node;
14968
+ if (typeof ref === 'function') {
14969
+ ref(node);
14970
+ }
14971
+ else if (ref) {
14972
+ ref.current = node;
14973
+ }
14974
+ };
14975
+ const sideMenuClass = generateSideMenuClass({
14976
+ className,
14977
+ isOpen: isOpenState,
14978
+ });
14979
+ const wrapperClass = generateWrapperClass();
14980
+ // Default toggle icon using Atomix Icon component
14981
+ const defaultToggleIcon = jsx(Icon, { name: "CaretRight", size: "xs" });
14982
+ // Determine if we should show toggler (mobile or desktop with collapsibleDesktop)
14983
+ const shouldShowToggler = (isMobileState && collapsible) || (!isMobileState && collapsibleDesktop);
14984
+ // Only show separate title if toggler is NOT shown (toggler already contains the title)
14985
+ const shouldShowTitle = title && !shouldShowToggler;
14986
+ const sideMenuContent = (jsxs(Fragment, { children: [title && shouldShowToggler && (jsxs("div", { className: "c-side-menu__toggler", onClick: handleToggle, role: "button", tabIndex: disabled ? -1 : 0, "aria-expanded": isOpenState, "aria-controls": id ? `${id}-content` : undefined, "aria-disabled": disabled, onKeyDown: e => {
14987
+ if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
14988
+ e.preventDefault();
14989
+ handleToggle();
14990
+ }
14991
+ }, children: [jsx("span", { className: "c-side-menu__title", children: title }), jsx("span", { className: "c-side-menu__toggler-icon", children: toggleIcon || defaultToggleIcon })] })), shouldShowTitle && jsx("h3", { className: "c-side-menu__title", children: title }), jsx("div", { ref: wrapperRef, className: wrapperClass, id: id ? `${id}-content` : undefined, "aria-hidden": shouldShowToggler ? !isOpenState : false, children: jsxs("div", { ref: innerRef, className: "c-side-menu__inner", children: [children && children, menuItems?.map((item, index) => {
14992
+ const isNestedItemOpen = nestedItemStates[index] ?? true;
14993
+ const hasItems = item.items && item.items.length > 0;
14994
+ const canToggle = hasItems && !disabled;
14995
+ const handleNestedToggle = () => {
14996
+ if (!canToggle)
14997
+ return;
14998
+ setNestedItemStates(prev => ({
14999
+ ...prev,
15000
+ [index]: !prev[index],
15001
+ }));
15002
+ };
15003
+ return (jsxs("div", { className: "c-side-menu__item", children: [item.title && (jsxs("div", { className: [
15004
+ 'c-side-menu__toggler',
15005
+ canToggle && 'c-side-menu__toggler--nested',
15006
+ isNestedItemOpen && 'is-open',
15007
+ ]
15008
+ .filter(Boolean)
15009
+ .join(' '), onClick: canToggle ? handleNestedToggle : undefined, role: canToggle ? 'button' : undefined, tabIndex: canToggle && !disabled ? 0 : undefined, "aria-expanded": canToggle ? isNestedItemOpen : undefined, "aria-disabled": disabled, onKeyDown: canToggle
15010
+ ? e => {
15011
+ if ((e.key === 'Enter' || e.key === ' ') && !disabled) {
15012
+ e.preventDefault();
15013
+ handleNestedToggle();
15014
+ }
15015
+ }
15016
+ : undefined, children: [jsx("span", { className: "c-side-menu__title", children: item.title }), canToggle && (jsx("span", { className: "c-side-menu__toggler-icon", children: item.toggleIcon || jsx(Icon, { name: "CaretRight", size: "xs" }) }))] })), hasItems && (jsx("div", { ref: node => {
15017
+ nestedWrapperRefs.current[index] = node;
15018
+ }, className: "c-side-menu__nested-wrapper", children: jsx("div", { ref: node => {
15019
+ nestedInnerRefs.current[index] = node;
15020
+ }, className: "c-side-menu__nested-inner", children: jsx(SideMenuList, { children: item.items?.map((subItem, subIndex) => (jsx(SideMenuItem, { href: subItem.href, onClick: subItem.onClick, active: subItem.active, disabled: subItem.disabled, icon: subItem.icon, children: subItem.title }, subIndex))) }) }) }))] }, index));
15021
+ })] }) })] }));
15022
+ if (glass) {
15023
+ const defaultGlassProps = {
15024
+ displacementScale: 70,
15025
+ blurAmount: 2,
15026
+ cornerRadius: 12,
15027
+ mode: 'shader',
15028
+ };
15029
+ const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
15030
+ return (jsx(AtomixGlass, { ...glassProps, children: jsx("div", { ref: combinedRef, className: sideMenuClass + ' c-side-menu--glass', id: id, style: style, children: sideMenuContent }) }));
15031
+ }
15032
+ return (jsx("div", { ref: combinedRef, className: sideMenuClass, id: id, style: style, children: sideMenuContent }));
14503
15033
  });
14504
- SideMenuList.displayName = 'SideMenuList';
15034
+ SideMenu.displayName = 'SideMenu';
14505
15035
 
14506
15036
  const Menu = forwardRef(({ children, className = '', style, disabled = false }, ref) => {
14507
15037
  return (jsx("div", { ref: ref, className: `c-menu ${className}`, style: style, children: jsx("ul", { className: "c-menu__list", role: "menu", children: React.Children.map(children, child => {
@@ -16786,6 +17316,7 @@ Toggle.displayName = 'Toggle';
16786
17316
  const Tooltip = ({ content, children, position = TOOLTIP.DEFAULTS.POSITION, trigger = TOOLTIP.DEFAULTS.TRIGGER, className = '', style, delay = TOOLTIP.DEFAULTS.DELAY, offset = TOOLTIP.DEFAULTS.OFFSET, glass, }) => {
16787
17317
  const [isVisible, setIsVisible] = useState(false);
16788
17318
  const timeoutRef = useRef(null);
17319
+ const tooltipId = React.useId();
16789
17320
  const showTooltip = () => {
16790
17321
  if (timeoutRef.current) {
16791
17322
  clearTimeout(timeoutRef.current);
@@ -16814,49 +17345,50 @@ const Tooltip = ({ content, children, position = TOOLTIP.DEFAULTS.POSITION, trig
16814
17345
  }
16815
17346
  };
16816
17347
  const getTooltipPositionClasses = () => {
16817
- switch (position) {
16818
- case 'top':
16819
- return 'c-tooltip--top';
16820
- case 'bottom':
16821
- return 'c-tooltip--bottom';
16822
- case 'left':
16823
- return 'c-tooltip--left';
16824
- case 'right':
16825
- return 'c-tooltip--right';
16826
- case 'top-left':
16827
- return 'c-tooltip--top-left';
16828
- case 'top-right':
16829
- return 'c-tooltip--top-right';
16830
- case 'bottom-left':
16831
- return 'c-tooltip--bottom-left';
16832
- case 'bottom-right':
16833
- return 'c-tooltip--bottom-right';
16834
- default:
16835
- return 'c-tooltip--top';
16836
- }
17348
+ const positionMap = {
17349
+ top: 'c-tooltip--top',
17350
+ bottom: 'c-tooltip--bottom',
17351
+ left: 'c-tooltip--left',
17352
+ right: 'c-tooltip--right',
17353
+ 'top-left': 'c-tooltip--top-left',
17354
+ 'top-right': 'c-tooltip--top-right',
17355
+ 'bottom-left': 'c-tooltip--bottom-left',
17356
+ 'bottom-right': 'c-tooltip--bottom-right',
17357
+ };
17358
+ return positionMap[position] || 'c-tooltip--top';
17359
+ };
17360
+ const wrapperProps = {};
17361
+ const triggerProps = {
17362
+ 'aria-describedby': isVisible ? tooltipId : undefined,
16837
17363
  };
16838
- const triggerProps = {};
16839
17364
  if (trigger === 'hover') {
16840
- triggerProps.onMouseEnter = showTooltip;
16841
- triggerProps.onMouseLeave = hideTooltip;
17365
+ wrapperProps.onMouseEnter = showTooltip;
17366
+ wrapperProps.onMouseLeave = hideTooltip;
17367
+ triggerProps.onFocus = showTooltip;
17368
+ triggerProps.onBlur = hideTooltip;
16842
17369
  }
16843
17370
  else if (trigger === 'click') {
16844
17371
  triggerProps.onClick = toggleTooltip;
17372
+ // For click trigger, we might want to handle keyboard activation too, but div isn't focusable by default.
17373
+ // Ideally the child should be a button.
16845
17374
  }
16846
- return (jsxs("div", { className: "u-position-relative u-d-inline-block", style: style, children: [jsx("div", { className: `${TOOLTIP.SELECTORS.TRIGGER.substring(1)}${className ? ` ${className}` : ''}`, ...triggerProps, children: children }), isVisible && (jsx("div", { className: `c-tooltip ${TOOLTIP.SELECTORS.TOOLTIP.substring(1)} ${getTooltipPositionClasses()} ${glass ? 'c-tooltip--glass' : ''}`, "data-tooltip-position": position, "data-tooltip-trigger": trigger, children: glass ? (
16847
- // Default glass settings for tooltips
16848
- (() => {
16849
- const defaultGlassProps = {
16850
- displacementScale: 40,
16851
- blurAmount: 1,
16852
- saturation: 160,
16853
- aberrationIntensity: 0.3,
16854
- cornerRadius: 6,
16855
- mode: 'shader',
16856
- };
16857
- const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
16858
- return (jsx(AtomixGlass, { ...glassProps, children: jsxs("div", { className: `c-tooltip__content ${TOOLTIP.SELECTORS.CONTENT.substring(1)} ${isVisible && 'is-active'}`, children: [jsx("span", { className: TOOLTIP.SELECTORS.ARROW.substring(1) }), content] }) }));
16859
- })()) : (jsxs("div", { className: `c-tooltip__content ${TOOLTIP.SELECTORS.CONTENT.substring(1)} ${isVisible && 'is-active'}`, children: [jsx("span", { className: TOOLTIP.SELECTORS.ARROW.substring(1) }), content] })) }))] }));
17375
+ const renderContent = () => {
17376
+ const contentElement = (jsxs("div", { className: `c-tooltip__content ${TOOLTIP.SELECTORS.CONTENT.substring(1)} ${isVisible && 'is-active'}`, children: [jsx("span", { className: TOOLTIP.SELECTORS.ARROW.substring(1) }), content] }));
17377
+ if (glass) {
17378
+ const defaultGlassProps = {
17379
+ displacementScale: 40,
17380
+ blurAmount: 1,
17381
+ saturation: 160,
17382
+ aberrationIntensity: 0.3,
17383
+ cornerRadius: 6,
17384
+ mode: 'shader',
17385
+ };
17386
+ const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
17387
+ return jsx(AtomixGlass, { ...glassProps, children: contentElement });
17388
+ }
17389
+ return contentElement;
17390
+ };
17391
+ return (jsxs("div", { className: "u-position-relative u-d-inline-block", style: style, ...wrapperProps, children: [jsx("div", { className: `${TOOLTIP.SELECTORS.TRIGGER.substring(1)}${className ? ` ${className}` : ''}`, ...triggerProps, children: children }), isVisible && (jsx("div", { id: tooltipId, role: "tooltip", className: `c-tooltip ${TOOLTIP.SELECTORS.TOOLTIP.substring(1)} ${getTooltipPositionClasses()} ${glass ? 'c-tooltip--glass' : ''}`, "data-tooltip-position": position, "data-tooltip-trigger": trigger, style: { '--atomix-tooltip-offset': `${offset}px` }, children: renderContent() }))] }));
16860
17392
  };
16861
17393
  Tooltip.displayName = 'Tooltip';
16862
17394
 
@@ -17866,12 +18398,1010 @@ const constantsImport = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.definePro
17866
18398
  sliderConstants
17867
18399
  }, Symbol.toStringTag, { value: 'Module' }));
17868
18400
 
18401
+ /**
18402
+ * Theme Manager Utility Functions
18403
+ *
18404
+ * Helper functions for theme operations including CSS loading, DOM manipulation,
18405
+ * and theme validation.
18406
+ */
18407
+ /**
18408
+ * Check if code is running in a browser environment
18409
+ */
18410
+ const isBrowser = () => {
18411
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
18412
+ };
18413
+ /**
18414
+ * Check if code is running on the server (SSR)
18415
+ */
18416
+ const isServer = () => {
18417
+ return !isBrowser();
18418
+ };
18419
+ /**
18420
+ * Generate a unique ID for theme link elements
18421
+ */
18422
+ const getThemeLinkId = (themeName) => {
18423
+ return `atomix-theme-${themeName}`;
18424
+ };
18425
+ /**
18426
+ * Build the CSS file path for a theme
18427
+ *
18428
+ * @param themeName - Name of the theme
18429
+ * @param basePath - Base path for theme files
18430
+ * @param useMinified - Whether to use minified CSS
18431
+ * @param cdnPath - Optional CDN path
18432
+ * @returns Full path to the theme CSS file
18433
+ */
18434
+ const buildThemePath = (themeName, basePath = '/themes', useMinified = false, cdnPath = null) => {
18435
+ // Validate theme name to prevent path injection
18436
+ if (!isValidThemeName(themeName)) {
18437
+ throw new Error(`Invalid theme name: "${themeName}". Theme names must be lowercase alphanumeric with hyphens.`);
18438
+ }
18439
+ const extension = useMinified ? '.min.css' : '.css';
18440
+ const fileName = `${themeName}${extension}`;
18441
+ if (cdnPath) {
18442
+ // Validate CDN path doesn't contain dangerous characters
18443
+ const cleanCdnPath = cdnPath.replace(/[<>"']/g, '');
18444
+ return `${cleanCdnPath}/${fileName}`;
18445
+ }
18446
+ // Ensure basePath doesn't end with slash and fileName doesn't start with slash
18447
+ // Also sanitize basePath to prevent path injection
18448
+ const cleanBasePath = basePath.replace(/\/$/, '').replace(/[<>"']/g, '');
18449
+ const cleanFileName = fileName.replace(/^\//, '');
18450
+ return `${cleanBasePath}/${cleanFileName}`;
18451
+ };
18452
+ /**
18453
+ * Load theme CSS file dynamically
18454
+ *
18455
+ * @param themeName - Name of the theme to load
18456
+ * @param basePath - Base path for theme files
18457
+ * @param useMinified - Whether to use minified CSS
18458
+ * @param cdnPath - Optional CDN path
18459
+ * @returns Promise that resolves when CSS is loaded
18460
+ */
18461
+ const loadThemeCSS = (themeName, basePath = '/themes', useMinified = false, cdnPath = null) => {
18462
+ if (isServer()) {
18463
+ return Promise.resolve();
18464
+ }
18465
+ return new Promise((resolve, reject) => {
18466
+ const linkId = getThemeLinkId(themeName);
18467
+ // Check if theme is already loaded
18468
+ const existingLink = document.getElementById(linkId);
18469
+ if (existingLink) {
18470
+ resolve();
18471
+ return;
18472
+ }
18473
+ // Create link element
18474
+ const link = document.createElement('link');
18475
+ link.id = linkId;
18476
+ link.rel = 'stylesheet';
18477
+ link.type = 'text/css';
18478
+ link.href = buildThemePath(themeName, basePath, useMinified, cdnPath);
18479
+ // Add data attribute for tracking
18480
+ link.setAttribute('data-atomix-theme', themeName);
18481
+ // Handle load success
18482
+ link.onload = () => {
18483
+ resolve();
18484
+ };
18485
+ // Handle load error
18486
+ link.onerror = () => {
18487
+ // Remove failed link element
18488
+ link.remove();
18489
+ reject(new Error(`Failed to load theme: ${themeName}`));
18490
+ };
18491
+ // Append to head
18492
+ document.head.appendChild(link);
18493
+ });
18494
+ };
18495
+ /**
18496
+ * Remove theme CSS from the DOM
18497
+ *
18498
+ * @param themeName - Name of the theme to remove
18499
+ */
18500
+ const removeThemeCSS = (themeName) => {
18501
+ if (isServer()) {
18502
+ return;
18503
+ }
18504
+ const linkId = getThemeLinkId(themeName);
18505
+ const link = document.getElementById(linkId);
18506
+ if (link) {
18507
+ link.remove();
18508
+ }
18509
+ };
18510
+ /**
18511
+ * Remove all theme CSS files from the DOM
18512
+ */
18513
+ const removeAllThemeCSS = () => {
18514
+ if (isServer()) {
18515
+ return;
18516
+ }
18517
+ const themeLinks = document.querySelectorAll('link[data-atomix-theme]');
18518
+ themeLinks.forEach(link => link.remove());
18519
+ };
18520
+ /**
18521
+ * Apply theme data attributes to the document
18522
+ *
18523
+ * @param themeName - Name of the theme
18524
+ * @param dataAttribute - Data attribute name (default: 'data-theme')
18525
+ */
18526
+ const applyThemeAttributes = (themeName, dataAttribute = 'data-theme') => {
18527
+ if (isServer()) {
18528
+ return;
18529
+ }
18530
+ // Set data attribute on body
18531
+ document.body.setAttribute(dataAttribute, themeName);
18532
+ // Also set on documentElement for broader compatibility
18533
+ document.documentElement.setAttribute(dataAttribute, themeName);
18534
+ };
18535
+ /**
18536
+ * Remove theme data attributes from the document
18537
+ *
18538
+ * @param dataAttribute - Data attribute name (default: 'data-theme')
18539
+ */
18540
+ const removeThemeAttributes = (dataAttribute = 'data-theme') => {
18541
+ if (isServer()) {
18542
+ return;
18543
+ }
18544
+ document.body.removeAttribute(dataAttribute);
18545
+ document.documentElement.removeAttribute(dataAttribute);
18546
+ };
18547
+ /**
18548
+ * Get the current theme from data attributes
18549
+ *
18550
+ * @param dataAttribute - Data attribute name (default: 'data-theme')
18551
+ * @returns Current theme name or null
18552
+ */
18553
+ const getCurrentThemeFromDOM = (dataAttribute = 'data-theme') => {
18554
+ if (isServer()) {
18555
+ return null;
18556
+ }
18557
+ return document.body.getAttribute(dataAttribute) ||
18558
+ document.documentElement.getAttribute(dataAttribute);
18559
+ };
18560
+ /**
18561
+ * Detect system theme preference
18562
+ *
18563
+ * @returns 'dark' if system prefers dark mode, 'light' otherwise
18564
+ */
18565
+ const getSystemTheme = () => {
18566
+ if (isServer()) {
18567
+ return 'light';
18568
+ }
18569
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
18570
+ return 'dark';
18571
+ }
18572
+ return 'light';
18573
+ };
18574
+ /**
18575
+ * Check if a theme is currently loaded in the DOM
18576
+ *
18577
+ * @param themeName - Name of the theme to check
18578
+ * @returns True if theme CSS is loaded
18579
+ */
18580
+ const isThemeLoaded = (themeName) => {
18581
+ if (isServer()) {
18582
+ return false;
18583
+ }
18584
+ const linkId = getThemeLinkId(themeName);
18585
+ return document.getElementById(linkId) !== null;
18586
+ };
18587
+ /**
18588
+ * Validate theme metadata
18589
+ *
18590
+ * @param metadata - Theme metadata to validate
18591
+ * @returns Validation result with errors and warnings
18592
+ */
18593
+ const validateThemeMetadata = (metadata) => {
18594
+ const errors = [];
18595
+ const warnings = [];
18596
+ if (!metadata || typeof metadata !== 'object') {
18597
+ errors.push('Theme metadata must be an object');
18598
+ return { valid: false, errors, warnings };
18599
+ }
18600
+ const theme = metadata;
18601
+ // Required fields
18602
+ if (!theme.name || typeof theme.name !== 'string') {
18603
+ errors.push('Theme must have a valid name');
18604
+ }
18605
+ // Optional but recommended fields
18606
+ if (!theme.description) {
18607
+ warnings.push('Theme should have a description');
18608
+ }
18609
+ if (!theme.version) {
18610
+ warnings.push('Theme should have a version');
18611
+ }
18612
+ if (!theme.author) {
18613
+ warnings.push('Theme should have an author');
18614
+ }
18615
+ // Validate status if provided
18616
+ if (theme.status) {
18617
+ const validStatuses = ['stable', 'beta', 'experimental', 'deprecated'];
18618
+ if (!validStatuses.includes(theme.status)) {
18619
+ errors.push(`Invalid status: ${theme.status}. Must be one of: ${validStatuses.join(', ')}`);
18620
+ }
18621
+ }
18622
+ // Validate color if provided
18623
+ if (theme.color && typeof theme.color !== 'string') {
18624
+ errors.push('Theme color must be a string');
18625
+ }
18626
+ // Validate a11y if provided
18627
+ if (theme.a11y) {
18628
+ if (typeof theme.a11y !== 'object') {
18629
+ errors.push('Theme a11y must be an object');
18630
+ }
18631
+ else {
18632
+ if (theme.a11y.contrastTarget !== undefined) {
18633
+ if (typeof theme.a11y.contrastTarget !== 'number' || theme.a11y.contrastTarget < 0) {
18634
+ errors.push('Theme a11y.contrastTarget must be a positive number');
18635
+ }
18636
+ }
18637
+ if (theme.a11y.modes !== undefined) {
18638
+ if (!Array.isArray(theme.a11y.modes)) {
18639
+ errors.push('Theme a11y.modes must be an array');
18640
+ }
18641
+ }
18642
+ }
18643
+ }
18644
+ return {
18645
+ valid: errors.length === 0,
18646
+ errors,
18647
+ warnings,
18648
+ };
18649
+ };
18650
+ /**
18651
+ * Validate theme name format
18652
+ *
18653
+ * @param themeName - Theme name to validate
18654
+ * @returns True if valid
18655
+ */
18656
+ const isValidThemeName = (themeName) => {
18657
+ if (!themeName || typeof themeName !== 'string') {
18658
+ return false;
18659
+ }
18660
+ // Theme names should be lowercase alphanumeric with hyphens
18661
+ const validPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
18662
+ return validPattern.test(themeName);
18663
+ };
18664
+ /**
18665
+ * Create a storage adapter for localStorage
18666
+ */
18667
+ const createLocalStorageAdapter = () => {
18668
+ return {
18669
+ getItem: (key) => {
18670
+ if (isServer())
18671
+ return null;
18672
+ try {
18673
+ return localStorage.getItem(key);
18674
+ }
18675
+ catch {
18676
+ return null;
18677
+ }
18678
+ },
18679
+ setItem: (key, value) => {
18680
+ if (isServer())
18681
+ return;
18682
+ try {
18683
+ localStorage.setItem(key, value);
18684
+ }
18685
+ catch {
18686
+ // Silently fail if localStorage is not available
18687
+ }
18688
+ },
18689
+ removeItem: (key) => {
18690
+ if (isServer())
18691
+ return;
18692
+ try {
18693
+ localStorage.removeItem(key);
18694
+ }
18695
+ catch {
18696
+ // Silently fail
18697
+ }
18698
+ },
18699
+ isAvailable: () => {
18700
+ if (isServer())
18701
+ return false;
18702
+ try {
18703
+ const test = '__atomix_storage_test__';
18704
+ localStorage.setItem(test, test);
18705
+ localStorage.removeItem(test);
18706
+ return true;
18707
+ }
18708
+ catch {
18709
+ return false;
18710
+ }
18711
+ },
18712
+ };
18713
+ };
18714
+ /**
18715
+ * Debounce function for performance optimization
18716
+ *
18717
+ * @param func - Function to debounce
18718
+ * @param wait - Wait time in milliseconds
18719
+ * @returns Debounced function
18720
+ */
18721
+ const debounce = (func, wait) => {
18722
+ let timeout = null;
18723
+ return function executedFunction(...args) {
18724
+ const later = () => {
18725
+ timeout = null;
18726
+ func(...args);
18727
+ };
18728
+ if (timeout !== null) {
18729
+ clearTimeout(timeout);
18730
+ }
18731
+ timeout = setTimeout(later, wait);
18732
+ };
18733
+ };
18734
+
18735
+ /**
18736
+ * Theme Manager
18737
+ *
18738
+ * Core theme management class for the Atomix Design System.
18739
+ * Handles theme loading, switching, persistence, and events.
18740
+ */
18741
+ /**
18742
+ * Default configuration values
18743
+ */
18744
+ const DEFAULT_CONFIG = {
18745
+ basePath: '/themes',
18746
+ cdnPath: null,
18747
+ lazy: true,
18748
+ storageKey: 'atomix-theme',
18749
+ dataAttribute: 'data-theme',
18750
+ enablePersistence: true,
18751
+ useMinified: false,
18752
+ preload: [],
18753
+ };
18754
+ /**
18755
+ * ThemeManager class
18756
+ *
18757
+ * Manages theme loading, switching, and persistence for Atomix Design System.
18758
+ *
18759
+ * @example
18760
+ * ```typescript
18761
+ * const themeManager = new ThemeManager({
18762
+ * themes: themesConfig.metadata,
18763
+ * defaultTheme: 'shaj-default',
18764
+ * });
18765
+ *
18766
+ * await themeManager.setTheme('flashtrade');
18767
+ * ```
18768
+ */
18769
+ class ThemeManager {
18770
+ /**
18771
+ * Create a new ThemeManager instance
18772
+ *
18773
+ * @param config - Theme manager configuration
18774
+ */
18775
+ constructor(config) {
18776
+ this.currentTheme = null;
18777
+ this.loadedThemes = new Set();
18778
+ this.loadingThemes = new Map();
18779
+ this.eventListeners = {
18780
+ themeChange: [],
18781
+ themeLoad: [],
18782
+ themeError: [],
18783
+ };
18784
+ this.initialized = false;
18785
+ // Validate required config
18786
+ if (!config.themes || Object.keys(config.themes).length === 0) {
18787
+ throw new Error('ThemeManager: themes configuration is required');
18788
+ }
18789
+ // Merge with defaults
18790
+ this.config = {
18791
+ ...DEFAULT_CONFIG,
18792
+ ...config,
18793
+ themes: config.themes,
18794
+ defaultTheme: config.defaultTheme || Object.keys(config.themes)[0],
18795
+ };
18796
+ // Validate default theme exists
18797
+ if (!this.config.themes[this.config.defaultTheme]) {
18798
+ throw new Error(`ThemeManager: default theme "${this.config.defaultTheme}" not found in themes configuration`);
18799
+ }
18800
+ // Initialize storage adapter
18801
+ this.storageAdapter = createLocalStorageAdapter();
18802
+ // Initialize theme manager
18803
+ this.initialize();
18804
+ }
18805
+ /**
18806
+ * Initialize the theme manager
18807
+ */
18808
+ initialize() {
18809
+ if (this.initialized || isServer()) {
18810
+ return;
18811
+ }
18812
+ // Try to load theme from storage
18813
+ let initialTheme = this.config.defaultTheme;
18814
+ if (this.config.enablePersistence && this.storageAdapter.isAvailable()) {
18815
+ const storedTheme = this.storageAdapter.getItem(this.config.storageKey);
18816
+ if (storedTheme && this.config.themes[storedTheme]) {
18817
+ initialTheme = storedTheme;
18818
+ }
18819
+ }
18820
+ // Check if theme is already set in DOM
18821
+ const domTheme = getCurrentThemeFromDOM(this.config.dataAttribute);
18822
+ if (domTheme && this.config.themes[domTheme]) {
18823
+ initialTheme = domTheme;
18824
+ }
18825
+ // Set initial theme
18826
+ this.currentTheme = initialTheme;
18827
+ // Preload themes if configured
18828
+ if (this.config.preload && this.config.preload.length > 0) {
18829
+ this.config.preload.forEach(themeName => {
18830
+ if (this.config.themes[themeName]) {
18831
+ this.preloadTheme(themeName).catch(error => {
18832
+ console.warn(`Failed to preload theme "${themeName}":`, error);
18833
+ });
18834
+ }
18835
+ });
18836
+ }
18837
+ this.initialized = true;
18838
+ }
18839
+ /**
18840
+ * Get the current theme name
18841
+ *
18842
+ * @returns Current theme name
18843
+ */
18844
+ getTheme() {
18845
+ return this.currentTheme || this.config.defaultTheme;
18846
+ }
18847
+ /**
18848
+ * Get all available themes
18849
+ *
18850
+ * @returns Array of theme metadata
18851
+ */
18852
+ getAvailableThemes() {
18853
+ return Object.entries(this.config.themes).map(([key, metadata]) => ({
18854
+ ...metadata,
18855
+ class: key,
18856
+ }));
18857
+ }
18858
+ /**
18859
+ * Get metadata for a specific theme
18860
+ *
18861
+ * @param themeName - Name of the theme
18862
+ * @returns Theme metadata or null if not found
18863
+ */
18864
+ getThemeMetadata(themeName) {
18865
+ const metadata = this.config.themes[themeName];
18866
+ return metadata ? { ...metadata, class: themeName } : null;
18867
+ }
18868
+ /**
18869
+ * Check if a theme is currently loaded
18870
+ *
18871
+ * @param themeName - Name of the theme to check
18872
+ * @returns True if theme is loaded
18873
+ */
18874
+ isThemeLoaded(themeName) {
18875
+ if (isServer()) {
18876
+ return false;
18877
+ }
18878
+ return this.loadedThemes.has(themeName) || isThemeLoaded(themeName);
18879
+ }
18880
+ /**
18881
+ * Validate a theme name
18882
+ *
18883
+ * @param themeName - Theme name to validate
18884
+ * @returns True if theme exists and is valid
18885
+ */
18886
+ validateTheme(themeName) {
18887
+ if (!isValidThemeName(themeName)) {
18888
+ return false;
18889
+ }
18890
+ const metadata = this.config.themes[themeName];
18891
+ if (!metadata) {
18892
+ return false;
18893
+ }
18894
+ const validation = validateThemeMetadata(metadata);
18895
+ return validation.valid;
18896
+ }
18897
+ /**
18898
+ * Preload a theme without applying it
18899
+ *
18900
+ * @param themeName - Name of the theme to preload
18901
+ * @returns Promise that resolves when theme is loaded
18902
+ */
18903
+ async preloadTheme(themeName) {
18904
+ if (isServer()) {
18905
+ return Promise.resolve();
18906
+ }
18907
+ // Validate theme name format to prevent path injection
18908
+ if (!isValidThemeName(themeName)) {
18909
+ const error = new Error(`Invalid theme name: "${themeName}". Theme names must be lowercase alphanumeric with hyphens.`);
18910
+ this.emitError(error, themeName);
18911
+ throw error;
18912
+ }
18913
+ // Check if theme exists
18914
+ if (!this.config.themes[themeName]) {
18915
+ const error = new Error(`Theme "${themeName}" not found`);
18916
+ this.emitError(error, themeName);
18917
+ throw error;
18918
+ }
18919
+ // Check if already loaded
18920
+ if (this.isThemeLoaded(themeName)) {
18921
+ return Promise.resolve();
18922
+ }
18923
+ // Check if already loading
18924
+ if (this.loadingThemes.has(themeName)) {
18925
+ return this.loadingThemes.get(themeName);
18926
+ }
18927
+ // Load theme CSS
18928
+ const loadPromise = loadThemeCSS(themeName, this.config.basePath, this.config.useMinified, this.config.cdnPath)
18929
+ .then(() => {
18930
+ this.loadedThemes.add(themeName);
18931
+ this.loadingThemes.delete(themeName);
18932
+ this.emitLoad(themeName);
18933
+ })
18934
+ .catch(error => {
18935
+ this.loadingThemes.delete(themeName);
18936
+ this.emitError(error, themeName);
18937
+ throw error;
18938
+ });
18939
+ this.loadingThemes.set(themeName, loadPromise);
18940
+ return loadPromise;
18941
+ }
18942
+ /**
18943
+ * Set the current theme
18944
+ *
18945
+ * @param themeName - Name of the theme to set
18946
+ * @param options - Load options
18947
+ * @returns Promise that resolves when theme is applied
18948
+ */
18949
+ async setTheme(themeName, options = {}) {
18950
+ if (isServer()) {
18951
+ return Promise.resolve();
18952
+ }
18953
+ // Validate theme name format to prevent path injection
18954
+ if (!isValidThemeName(themeName)) {
18955
+ const error = new Error(`Invalid theme name: "${themeName}". Theme names must be lowercase alphanumeric with hyphens.`);
18956
+ this.emitError(error, themeName);
18957
+ throw error;
18958
+ }
18959
+ // Validate theme exists
18960
+ if (!this.config.themes[themeName]) {
18961
+ const error = new Error(`Theme "${themeName}" not found`);
18962
+ this.emitError(error, themeName);
18963
+ throw error;
18964
+ }
18965
+ // Check if already current theme
18966
+ if (themeName === this.currentTheme && !options.force) {
18967
+ return Promise.resolve();
18968
+ }
18969
+ const previousTheme = this.currentTheme;
18970
+ try {
18971
+ // Load theme CSS if not already loaded
18972
+ if (!this.isThemeLoaded(themeName) || options.force) {
18973
+ await this.preloadTheme(themeName);
18974
+ }
18975
+ // Remove previous theme CSS if requested
18976
+ if (options.removePrevious && previousTheme && previousTheme !== themeName) {
18977
+ removeThemeCSS(previousTheme);
18978
+ this.loadedThemes.delete(previousTheme);
18979
+ }
18980
+ // Apply theme attributes
18981
+ applyThemeAttributes(themeName, this.config.dataAttribute);
18982
+ // Update current theme
18983
+ this.currentTheme = themeName;
18984
+ // Persist to storage
18985
+ if (this.config.enablePersistence && this.storageAdapter.isAvailable()) {
18986
+ this.storageAdapter.setItem(this.config.storageKey, themeName);
18987
+ }
18988
+ // Emit theme change event
18989
+ this.emitThemeChange(previousTheme, themeName);
18990
+ // Call config callback
18991
+ if (this.config.onThemeChange) {
18992
+ this.config.onThemeChange(themeName);
18993
+ }
18994
+ }
18995
+ catch (error) {
18996
+ const err = error instanceof Error ? error : new Error(String(error));
18997
+ this.emitError(err, themeName);
18998
+ if (this.config.onError) {
18999
+ this.config.onError(err, themeName);
19000
+ }
19001
+ // Fallback to default theme if requested
19002
+ if (options.fallbackOnError && themeName !== this.config.defaultTheme) {
19003
+ console.warn(`Failed to load theme "${themeName}", falling back to default theme "${this.config.defaultTheme}"`);
19004
+ return this.setTheme(this.config.defaultTheme, { ...options, fallbackOnError: false });
19005
+ }
19006
+ throw err;
19007
+ }
19008
+ }
19009
+ /**
19010
+ * Enable theme persistence
19011
+ *
19012
+ * @param storageKey - Optional custom storage key
19013
+ */
19014
+ enablePersistence(storageKey) {
19015
+ this.config.enablePersistence = true;
19016
+ if (storageKey) {
19017
+ this.config.storageKey = storageKey;
19018
+ }
19019
+ // Save current theme
19020
+ if (this.currentTheme && this.storageAdapter.isAvailable()) {
19021
+ this.storageAdapter.setItem(this.config.storageKey, this.currentTheme);
19022
+ }
19023
+ }
19024
+ /**
19025
+ * Disable theme persistence
19026
+ */
19027
+ disablePersistence() {
19028
+ this.config.enablePersistence = false;
19029
+ // Clear stored theme
19030
+ if (this.storageAdapter.isAvailable()) {
19031
+ this.storageAdapter.removeItem(this.config.storageKey);
19032
+ }
19033
+ }
19034
+ /**
19035
+ * Clear all loaded themes
19036
+ */
19037
+ clearThemes() {
19038
+ if (isServer()) {
19039
+ return;
19040
+ }
19041
+ removeAllThemeCSS();
19042
+ this.loadedThemes.clear();
19043
+ this.loadingThemes.clear();
19044
+ }
19045
+ on(event, callback) {
19046
+ if (event === 'themeChange') {
19047
+ this.eventListeners.themeChange.push(callback);
19048
+ }
19049
+ else if (event === 'themeLoad') {
19050
+ this.eventListeners.themeLoad.push(callback);
19051
+ }
19052
+ else if (event === 'themeError') {
19053
+ this.eventListeners.themeError.push(callback);
19054
+ }
19055
+ }
19056
+ off(event, callback) {
19057
+ if (event === 'themeChange') {
19058
+ this.eventListeners.themeChange = this.eventListeners.themeChange.filter(cb => cb !== callback);
19059
+ }
19060
+ else if (event === 'themeLoad') {
19061
+ this.eventListeners.themeLoad = this.eventListeners.themeLoad.filter(cb => cb !== callback);
19062
+ }
19063
+ else if (event === 'themeError') {
19064
+ this.eventListeners.themeError = this.eventListeners.themeError.filter(cb => cb !== callback);
19065
+ }
19066
+ }
19067
+ /**
19068
+ * Emit theme change event
19069
+ */
19070
+ emitThemeChange(previousTheme, currentTheme) {
19071
+ const event = {
19072
+ previousTheme,
19073
+ currentTheme,
19074
+ timestamp: Date.now(),
19075
+ source: 'user',
19076
+ };
19077
+ this.eventListeners.themeChange.forEach(callback => {
19078
+ try {
19079
+ callback(event);
19080
+ }
19081
+ catch (error) {
19082
+ console.error('Error in themeChange listener:', error);
19083
+ }
19084
+ });
19085
+ }
19086
+ /**
19087
+ * Emit theme load event
19088
+ */
19089
+ emitLoad(themeName) {
19090
+ this.eventListeners.themeLoad.forEach(callback => {
19091
+ try {
19092
+ callback(themeName);
19093
+ }
19094
+ catch (error) {
19095
+ console.error('Error in themeLoad listener:', error);
19096
+ }
19097
+ });
19098
+ }
19099
+ /**
19100
+ * Emit theme error event
19101
+ */
19102
+ emitError(error, themeName) {
19103
+ this.eventListeners.themeError.forEach(callback => {
19104
+ try {
19105
+ callback(error, themeName);
19106
+ }
19107
+ catch (err) {
19108
+ console.error('Error in themeError listener:', err);
19109
+ }
19110
+ });
19111
+ }
19112
+ /**
19113
+ * Destroy the theme manager and clean up
19114
+ */
19115
+ destroy() {
19116
+ this.clearThemes();
19117
+ this.eventListeners.themeChange = [];
19118
+ this.eventListeners.themeLoad = [];
19119
+ this.eventListeners.themeError = [];
19120
+ this.initialized = false;
19121
+ }
19122
+ }
19123
+
19124
+ /**
19125
+ * Theme Context
19126
+ *
19127
+ * React context for theme management
19128
+ */
19129
+ /**
19130
+ * Theme context with default values
19131
+ */
19132
+ const ThemeContext = createContext(null);
19133
+ ThemeContext.displayName = 'ThemeContext';
19134
+
19135
+ /**
19136
+ * ThemeProvider component
19137
+ *
19138
+ * Provides theme context to child components and manages theme state.
19139
+ *
19140
+ * @example
19141
+ * ```tsx
19142
+ * import { ThemeProvider } from '@shohojdhara/atomix/theme';
19143
+ * import { themesConfig } from '@shohojdhara/atomix/themes/themes.config';
19144
+ *
19145
+ * function App() {
19146
+ * return (
19147
+ * <ThemeProvider
19148
+ * themes={themesConfig.metadata}
19149
+ * defaultTheme="shaj-default"
19150
+ * >
19151
+ * <YourApp />
19152
+ * </ThemeProvider>
19153
+ * );
19154
+ * }
19155
+ * ```
19156
+ */
19157
+ const ThemeProvider = ({ children, defaultTheme = 'shaj-default', themes = {}, basePath = '/themes', cdnPath = null, preload = [], lazy = true, storageKey = 'atomix-theme', dataAttribute = 'data-theme', enablePersistence = true, useMinified = false, onThemeChange, onError, }) => {
19158
+ // Initialize theme manager
19159
+ const themeManager = useMemo(() => {
19160
+ try {
19161
+ return new ThemeManager({
19162
+ themes,
19163
+ defaultTheme,
19164
+ basePath,
19165
+ cdnPath,
19166
+ preload,
19167
+ lazy,
19168
+ storageKey,
19169
+ dataAttribute,
19170
+ enablePersistence,
19171
+ useMinified,
19172
+ onThemeChange,
19173
+ onError,
19174
+ });
19175
+ }
19176
+ catch (error) {
19177
+ console.error('Failed to initialize ThemeManager:', error);
19178
+ // Return a minimal fallback manager
19179
+ return new ThemeManager({
19180
+ themes: { [defaultTheme]: { name: defaultTheme } },
19181
+ defaultTheme,
19182
+ });
19183
+ }
19184
+ }, [
19185
+ themes,
19186
+ defaultTheme,
19187
+ basePath,
19188
+ cdnPath,
19189
+ preload,
19190
+ lazy,
19191
+ storageKey,
19192
+ dataAttribute,
19193
+ enablePersistence,
19194
+ useMinified,
19195
+ onThemeChange,
19196
+ onError,
19197
+ ]);
19198
+ // State
19199
+ const [currentTheme, setCurrentTheme] = useState(themeManager.getTheme());
19200
+ const [isLoading, setIsLoading] = useState(false);
19201
+ const [error, setError] = useState(null);
19202
+ // Get available themes
19203
+ const availableThemes = useMemo(() => themeManager.getAvailableThemes(), [themeManager]);
19204
+ // Set theme function
19205
+ const setTheme = useCallback(async (themeName, options) => {
19206
+ setIsLoading(true);
19207
+ setError(null);
19208
+ try {
19209
+ await themeManager.setTheme(themeName, options);
19210
+ setCurrentTheme(themeName);
19211
+ }
19212
+ catch (err) {
19213
+ const error = err instanceof Error ? err : new Error(String(err));
19214
+ setError(error);
19215
+ // If fallback is enabled and theme is not default, try to fallback
19216
+ if (options?.fallbackOnError && themeName !== defaultTheme) {
19217
+ try {
19218
+ await themeManager.setTheme(defaultTheme, { fallbackOnError: false });
19219
+ setCurrentTheme(defaultTheme);
19220
+ setError(null);
19221
+ return;
19222
+ }
19223
+ catch (fallbackErr) {
19224
+ // If fallback also fails, throw original error
19225
+ throw error;
19226
+ }
19227
+ }
19228
+ throw error;
19229
+ }
19230
+ finally {
19231
+ setIsLoading(false);
19232
+ }
19233
+ }, [themeManager, defaultTheme]);
19234
+ // Check if theme is loaded
19235
+ const isThemeLoaded = useCallback((themeName) => {
19236
+ return themeManager.isThemeLoaded(themeName);
19237
+ }, [themeManager]);
19238
+ // Preload theme
19239
+ const preloadTheme = useCallback(async (themeName) => {
19240
+ try {
19241
+ await themeManager.preloadTheme(themeName);
19242
+ }
19243
+ catch (err) {
19244
+ const error = err instanceof Error ? err : new Error(String(err));
19245
+ setError(error);
19246
+ throw error;
19247
+ }
19248
+ }, [themeManager]);
19249
+ // Listen for theme changes
19250
+ useEffect(() => {
19251
+ const handleThemeChange = () => {
19252
+ setCurrentTheme(themeManager.getTheme());
19253
+ };
19254
+ themeManager.on('themeChange', handleThemeChange);
19255
+ return () => {
19256
+ themeManager.off('themeChange', handleThemeChange);
19257
+ };
19258
+ }, [themeManager]);
19259
+ // Load initial theme
19260
+ useEffect(() => {
19261
+ const loadInitialTheme = async () => {
19262
+ setIsLoading(true);
19263
+ try {
19264
+ await themeManager.setTheme(themeManager.getTheme());
19265
+ }
19266
+ catch (err) {
19267
+ const error = err instanceof Error ? err : new Error(String(err));
19268
+ setError(error);
19269
+ console.error('Failed to load initial theme:', error);
19270
+ }
19271
+ finally {
19272
+ setIsLoading(false);
19273
+ }
19274
+ };
19275
+ loadInitialTheme();
19276
+ }, [themeManager]);
19277
+ // Cleanup on unmount
19278
+ useEffect(() => {
19279
+ return () => {
19280
+ themeManager.destroy();
19281
+ };
19282
+ }, [themeManager]);
19283
+ // Context value
19284
+ const contextValue = useMemo(() => ({
19285
+ theme: currentTheme,
19286
+ setTheme,
19287
+ availableThemes,
19288
+ isLoading,
19289
+ error,
19290
+ isThemeLoaded,
19291
+ preloadTheme,
19292
+ themeManager,
19293
+ }), [
19294
+ currentTheme,
19295
+ setTheme,
19296
+ availableThemes,
19297
+ isLoading,
19298
+ error,
19299
+ isThemeLoaded,
19300
+ preloadTheme,
19301
+ themeManager,
19302
+ ]);
19303
+ return (jsx(ThemeContext.Provider, { value: contextValue, children: children }));
19304
+ };
19305
+ ThemeProvider.displayName = 'ThemeProvider';
19306
+
19307
+ /**
19308
+ * useTheme Hook
19309
+ *
19310
+ * React hook for accessing and managing theme state
19311
+ */
19312
+ /**
19313
+ * useTheme hook
19314
+ *
19315
+ * Access theme context and manage theme state in React components.
19316
+ * Must be used within a ThemeProvider.
19317
+ *
19318
+ * @param options - Hook options
19319
+ * @returns Theme state and methods
19320
+ *
19321
+ * @example
19322
+ * ```tsx
19323
+ * function ThemeSwitcher() {
19324
+ * const { theme, setTheme, availableThemes, isLoading } = useTheme();
19325
+ *
19326
+ * return (
19327
+ * <select value={theme} onChange={(e) => setTheme(e.target.value)}>
19328
+ * {availableThemes.map(t => (
19329
+ * <option key={t.class} value={t.class}>{t.name}</option>
19330
+ * ))}
19331
+ * </select>
19332
+ * );
19333
+ * }
19334
+ * ```
19335
+ */
19336
+ const useTheme = (options = {}) => {
19337
+ const context = useContext(ThemeContext);
19338
+ if (!context) {
19339
+ throw new Error('useTheme must be used within a ThemeProvider. ' +
19340
+ 'Wrap your component tree with <ThemeProvider> to use this hook.');
19341
+ }
19342
+ const { theme, setTheme: contextSetTheme, availableThemes, isLoading, error, isThemeLoaded, preloadTheme, } = context;
19343
+ // Extract onChange callback to avoid dependency on entire options object
19344
+ const onChange = options?.onChange;
19345
+ // Wrap setTheme to call onChange callback if provided
19346
+ const setTheme = useCallback(async (themeName, themeOptions) => {
19347
+ await contextSetTheme(themeName, themeOptions);
19348
+ if (onChange) {
19349
+ onChange(themeName);
19350
+ }
19351
+ }, [contextSetTheme, onChange]);
19352
+ return {
19353
+ theme,
19354
+ setTheme,
19355
+ availableThemes,
19356
+ isLoading,
19357
+ error,
19358
+ isThemeLoaded,
19359
+ preloadTheme,
19360
+ };
19361
+ };
19362
+
19363
+ /**
19364
+ * Theme Module Entry Point
19365
+ *
19366
+ * Exports all theme management utilities for the Atomix Design System
19367
+ */
19368
+ // Core theme manager
19369
+
19370
+ const themeImport = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
19371
+ __proto__: null,
19372
+ ThemeContext,
19373
+ ThemeContextDefault: ThemeContext,
19374
+ ThemeManager,
19375
+ ThemeManagerDefault: ThemeManager,
19376
+ ThemeProvider,
19377
+ ThemeProviderDefault: ThemeProvider,
19378
+ applyThemeAttributes,
19379
+ buildThemePath,
19380
+ createLocalStorageAdapter,
19381
+ debounce,
19382
+ getCurrentThemeFromDOM,
19383
+ getSystemTheme,
19384
+ getThemeLinkId,
19385
+ isBrowser,
19386
+ isServer,
19387
+ isThemeLoaded,
19388
+ isValidThemeName,
19389
+ loadThemeCSS,
19390
+ removeAllThemeCSS,
19391
+ removeThemeAttributes,
19392
+ removeThemeCSS,
19393
+ useTheme,
19394
+ useThemeDefault: useTheme,
19395
+ validateThemeMetadata
19396
+ }, Symbol.toStringTag, { value: 'Module' }));
19397
+
17869
19398
  // Import and re-export as namespaces with proper typing
17870
19399
  // Export as namespaces with explicit typing
17871
19400
  const composables = composablesImport;
17872
19401
  const utils = utilsImport;
17873
19402
  const types = typesImport;
17874
19403
  const constants = constantsImport;
19404
+ const theme = themeImport;
17875
19405
 
17876
19406
  // Export all components individually for better tree-shaking
17877
19407
  const atomix = {
@@ -17884,5 +19414,5 @@ const atomix = {
17884
19414
  types,
17885
19415
  };
17886
19416
 
17887
- export { Accordion, AnimatedChart, AreaChart, AtomixGlass, AtomixLogo, Avatar, AvatarGroup, Badge, BarChart, Block, Breadcrumb, BubbleChart, Button, Callout, CandlestickChart, Card, Chart, ChartRenderer, Checkbox, ColorModeToggle, Container, Countdown, DataTable, DatePicker, DonutChart, Dropdown, EdgePanel, ElevationCard, Footer, FooterLink, FooterSection, FooterSocialLink, Form, FormGroup, FunnelChart, GaugeChart, Grid, GridCol, HeatmapChart, Hero, Icon, Input, LineChart, List, ListGroup, MasonryGrid, MasonryGridItem, MegaMenu, MegaMenuColumn, MegaMenuLink, Menu, MenuDivider, MenuItem, Messages, Modal, MultiAxisChart, Nav, NavDropdown, NavItem, Navbar, Pagination, PhotoViewer, PieChart, Popover, ProductReview, Progress, RadarChart, Radio, Rating, River, Row, ScatterChart, SectionIntro, Select, SideMenu, SideMenuItem, SideMenuList, Slider, Spinner, Steps, Tabs, Testimonial, Textarea, Todo, Toggle, Tooltip, TreemapChart, Upload, VideoPlayer, WaterfallChart, composables, constants, atomix as default, types, utils };
19417
+ export { Accordion, AnimatedChart, AreaChart, AtomixGlass, AtomixLogo, Avatar, AvatarGroup, Badge, BarChart, Block, Breadcrumb, BubbleChart, Button, Callout, CandlestickChart, Card, Chart, ChartRenderer, Checkbox, ColorModeToggle, Container, Countdown, DataTable, DatePicker, DonutChart, Dropdown, EdgePanel, ElevationCard, Footer, FooterLink, FooterSection, FooterSocialLink, Form, FormGroup, FunnelChart, GaugeChart, Grid, GridCol, HeatmapChart, Hero, Icon, Input, LineChart, List, ListGroup, MasonryGrid, MasonryGridItem, MegaMenu, MegaMenuColumn, MegaMenuLink, Menu, MenuDivider, MenuItem, Messages, Modal, MultiAxisChart, Nav, NavDropdown, NavItem, Navbar, Pagination, PhotoViewer, PieChart, Popover, ProductReview, Progress, RadarChart, Radio, Rating, River, Row, ScatterChart, SectionIntro, Select, SideMenu, SideMenuItem, SideMenuList, Slider, Spinner, Steps, Tabs, Testimonial, Textarea, ThemeContext, ThemeContext as ThemeContextDefault, ThemeManager, ThemeManager as ThemeManagerDefault, ThemeProvider, ThemeProvider as ThemeProviderDefault, Todo, Toggle, Tooltip, TreemapChart, Upload, VideoPlayer, WaterfallChart, applyThemeAttributes, buildThemePath, composables, constants, createLocalStorageAdapter, debounce, atomix as default, getCurrentThemeFromDOM, getSystemTheme, getThemeLinkId, isBrowser, isServer, isThemeLoaded, isValidThemeName, loadThemeCSS, removeAllThemeCSS, removeThemeAttributes, removeThemeCSS, theme, types, useTheme, useTheme as useThemeDefault, utils, validateThemeMetadata };
17888
19418
  //# sourceMappingURL=index.esm.js.map