@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.
- package/CHANGELOG.md +58 -0
- package/README.md +40 -1
- package/dist/atomix.css +412 -77
- package/dist/atomix.min.css +3 -3
- package/dist/index.d.ts +913 -12
- package/dist/index.esm.js +1739 -209
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1763 -208
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/themes/applemix.css +412 -77
- package/dist/themes/applemix.min.css +3 -3
- package/dist/themes/boomdevs.css +411 -76
- package/dist/themes/boomdevs.min.css +3 -3
- package/dist/themes/esrar.css +412 -77
- package/dist/themes/esrar.min.css +3 -3
- package/dist/themes/flashtrade.css +1803 -622
- package/dist/themes/flashtrade.min.css +113 -7
- package/dist/themes/mashroom.css +411 -76
- package/dist/themes/mashroom.min.css +4 -4
- package/dist/themes/shaj-default.css +411 -76
- package/dist/themes/shaj-default.min.css +3 -3
- package/package.json +13 -2
- package/src/components/Button/Button.stories.tsx +174 -0
- package/src/components/Button/Button.tsx +238 -78
- package/src/components/Card/Card.stories.tsx +202 -0
- package/src/components/Card/Card.tsx +253 -77
- package/src/components/Form/Input.stories.tsx +228 -2
- package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +301 -13
- package/src/components/Navigation/SideMenu/SideMenu.tsx +236 -9
- package/src/components/Tooltip/Tooltip.tsx +68 -66
- package/src/lib/composables/useButton.ts +37 -5
- package/src/lib/composables/useInput.ts +39 -1
- package/src/lib/composables/useSideMenu.ts +89 -30
- package/src/lib/constants/components.ts +53 -0
- package/src/lib/index.ts +5 -0
- package/src/lib/theme/ThemeContext.tsx +17 -0
- package/src/lib/theme/ThemeManager.stories.tsx +472 -0
- package/src/lib/theme/ThemeManager.test.ts +186 -0
- package/src/lib/theme/ThemeManager.ts +501 -0
- package/src/lib/theme/ThemeProvider.tsx +227 -0
- package/src/lib/theme/index.ts +56 -0
- package/src/lib/theme/types.ts +247 -0
- package/src/lib/theme/useTheme.test.tsx +66 -0
- package/src/lib/theme/useTheme.ts +80 -0
- package/src/lib/theme/utils.test.ts +140 -0
- package/src/lib/theme/utils.ts +398 -0
- package/src/lib/types/components.ts +304 -4
- package/src/styles/01-settings/_settings.tooltip.scss +2 -2
- package/src/styles/06-components/_components.button.scss +100 -0
- package/src/styles/06-components/_components.card.scss +235 -2
- package/src/styles/06-components/_components.side-menu.scss +79 -18
- 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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
4309
|
-
const
|
|
4310
|
-
|
|
4311
|
-
|
|
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
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
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
|
-
|
|
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(({
|
|
4430
|
-
|
|
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
|
-
|
|
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 (
|
|
4800
|
+
return (jsx(AtomixGlass, { ...glassProps, elasticity: 0, children: divElement }));
|
|
4443
4801
|
}
|
|
4444
|
-
return
|
|
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.
|
|
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
|
-
|
|
11572
|
-
|
|
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
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
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 (
|
|
11590
|
-
// Set proper height for mobile
|
|
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
|
-
|
|
11593
|
-
|
|
11594
|
-
|
|
11595
|
-
|
|
11596
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
11606
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11612
|
-
|
|
11613
|
-
|
|
11614
|
-
|
|
11615
|
-
|
|
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
|
-
*
|
|
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
|
-
* <
|
|
14392
|
-
* <
|
|
14393
|
-
*
|
|
14394
|
-
*
|
|
14395
|
-
*
|
|
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
|
|
14401
|
-
const
|
|
14402
|
-
|
|
14403
|
-
|
|
14404
|
-
|
|
14405
|
-
|
|
14406
|
-
|
|
14407
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
* <
|
|
14489
|
-
* <
|
|
14490
|
-
*
|
|
14491
|
-
*
|
|
14492
|
-
* </
|
|
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
|
|
14496
|
-
const
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
|
|
16827
|
-
|
|
16828
|
-
|
|
16829
|
-
|
|
16830
|
-
|
|
16831
|
-
|
|
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
|
-
|
|
16841
|
-
|
|
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
|
-
|
|
16847
|
-
|
|
16848
|
-
|
|
16849
|
-
|
|
16850
|
-
|
|
16851
|
-
|
|
16852
|
-
|
|
16853
|
-
|
|
16854
|
-
|
|
16855
|
-
|
|
16856
|
-
|
|
16857
|
-
|
|
16858
|
-
|
|
16859
|
-
|
|
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
|