@mezzanine-ui/react 1.0.0-beta.3 → 1.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/AutoComplete/AutoComplete.d.ts +23 -0
  2. package/AutoComplete/AutoComplete.js +39 -9
  3. package/Breadcrumb/Breadcrumb.js +16 -21
  4. package/Breadcrumb/BreadcrumbDropdown.d.ts +11 -0
  5. package/Breadcrumb/BreadcrumbDropdown.js +22 -0
  6. package/Breadcrumb/BreadcrumbItem.d.ts +2 -3
  7. package/Breadcrumb/BreadcrumbItem.js +13 -31
  8. package/Breadcrumb/BreadcrumbOverflowMenu.d.ts +7 -0
  9. package/Breadcrumb/BreadcrumbOverflowMenu.js +77 -0
  10. package/Breadcrumb/BreadcrumbOverflowMenuDropdown.d.ts +11 -0
  11. package/Breadcrumb/BreadcrumbOverflowMenuDropdown.js +21 -0
  12. package/Breadcrumb/BreadcrumbOverflowMenuItem.d.ts +3 -0
  13. package/Breadcrumb/BreadcrumbOverflowMenuItem.js +27 -0
  14. package/Breadcrumb/typings.d.ts +21 -39
  15. package/Calendar/Calendar.js +2 -6
  16. package/Calendar/CalendarCell.d.ts +22 -0
  17. package/Calendar/CalendarCell.js +6 -2
  18. package/Calendar/CalendarControls.js +1 -1
  19. package/Calendar/CalendarDayOfWeek.js +3 -2
  20. package/Calendar/CalendarDays.js +5 -1
  21. package/Calendar/CalendarHalfYears.js +13 -7
  22. package/Calendar/CalendarMonths.js +13 -6
  23. package/Calendar/CalendarQuarters.js +13 -7
  24. package/Calendar/CalendarWeeks.js +87 -34
  25. package/Calendar/CalendarYears.js +13 -12
  26. package/Calendar/useCalendarControlModifiers.d.ts +1 -1
  27. package/Calendar/useCalendarControlModifiers.js +12 -12
  28. package/Calendar/useCalendarControls.d.ts +4 -4
  29. package/Calendar/useCalendarControls.js +33 -19
  30. package/Calendar/useRangeCalendarControls.d.ts +8 -8
  31. package/Calendar/useRangeCalendarControls.js +42 -31
  32. package/Checkbox/index.d.ts +4 -5
  33. package/Checkbox/index.js +1 -5
  34. package/ContentHeader/ContentHeader.d.ts +160 -0
  35. package/ContentHeader/ContentHeader.js +54 -0
  36. package/ContentHeader/index.d.ts +2 -0
  37. package/ContentHeader/index.js +1 -0
  38. package/ContentHeader/utils.d.ts +23 -0
  39. package/ContentHeader/utils.js +215 -0
  40. package/DateRangePicker/useDateRangeCalendarControls.js +8 -2
  41. package/Dropdown/Dropdown.d.ts +48 -0
  42. package/Dropdown/Dropdown.js +16 -2
  43. package/Dropdown/DropdownItem.d.ts +42 -0
  44. package/Dropdown/DropdownItem.js +144 -13
  45. package/Dropdown/DropdownItemCard.d.ts +7 -2
  46. package/Dropdown/DropdownItemCard.js +12 -9
  47. package/Empty/Empty.js +2 -1
  48. package/Empty/icons/EmptyMainNotificationIcon.d.ts +4 -0
  49. package/Empty/icons/EmptyMainNotificationIcon.js +9 -0
  50. package/Empty/typings.d.ts +2 -2
  51. package/FilterArea/Filter.d.ts +32 -0
  52. package/FilterArea/Filter.js +23 -0
  53. package/FilterArea/FilterArea.d.ts +58 -0
  54. package/FilterArea/FilterArea.js +31 -0
  55. package/FilterArea/FilterLine.d.ts +11 -0
  56. package/FilterArea/FilterLine.js +13 -0
  57. package/FilterArea/index.d.ts +6 -0
  58. package/FilterArea/index.js +3 -0
  59. package/Form/useSelectValueControl.d.ts +3 -4
  60. package/Form/useSelectValueControl.js +51 -39
  61. package/Input/Input.d.ts +6 -4
  62. package/Input/Input.js +28 -10
  63. package/Input/index.d.ts +1 -1
  64. package/Modal/MediaPreviewModal.d.ts +54 -0
  65. package/Modal/MediaPreviewModal.js +158 -0
  66. package/Modal/Modal.js +1 -1
  67. package/Modal/index.d.ts +2 -0
  68. package/Modal/index.js +1 -0
  69. package/Navigation/Navigation.js +6 -5
  70. package/Navigation/NavigationOption.d.ts +6 -2
  71. package/Navigation/NavigationOption.js +19 -9
  72. package/Navigation/NavigationOverflowMenu.d.ts +6 -0
  73. package/Navigation/NavigationOverflowMenu.js +90 -0
  74. package/Navigation/NavigationOverflowMenuOption.d.ts +7 -0
  75. package/Navigation/NavigationOverflowMenuOption.js +68 -0
  76. package/Navigation/NavigationUserMenu.d.ts +4 -2
  77. package/Navigation/NavigationUserMenu.js +13 -5
  78. package/Navigation/context.d.ts +3 -2
  79. package/NotificationCenter/NotificationCenter.d.ts +1 -1
  80. package/NotificationCenter/NotificationCenter.js +34 -14
  81. package/NotificationCenter/NotificationCenterDrawer.d.ts +20 -0
  82. package/PageHeader/PageHeader.d.ts +32 -25
  83. package/PageHeader/PageHeader.js +49 -35
  84. package/Popper/Popper.js +2 -1
  85. package/ResultState/ResultState.d.ts +9 -0
  86. package/ResultState/ResultState.js +36 -4
  87. package/Scrollbar/Scrollbar.d.ts +9 -0
  88. package/Scrollbar/Scrollbar.js +79 -0
  89. package/Scrollbar/index.d.ts +2 -0
  90. package/Scrollbar/index.js +1 -0
  91. package/Scrollbar/typings.d.ts +47 -0
  92. package/Select/Select.d.ts +37 -18
  93. package/Select/Select.js +165 -51
  94. package/Select/SelectTrigger.js +5 -4
  95. package/Select/index.d.ts +8 -9
  96. package/Select/index.js +3 -3
  97. package/Select/typings.d.ts +6 -1
  98. package/Selection/Selection.js +1 -1
  99. package/Selection/SelectionGroup.d.ts +28 -0
  100. package/Table/Table.d.ts +2 -120
  101. package/Table/Table.js +148 -53
  102. package/Table/TableContext.d.ts +11 -12
  103. package/Table/components/TableActionsCell.js +12 -4
  104. package/Table/components/TableBody.js +2 -1
  105. package/Table/components/TableColGroup.d.ts +1 -4
  106. package/Table/components/TableColGroup.js +15 -16
  107. package/Table/components/TableCollectableCell.d.ts +17 -0
  108. package/Table/components/TableCollectableCell.js +54 -0
  109. package/Table/components/TableDragOrPinHandleCell.d.ts +20 -0
  110. package/Table/components/TableDragOrPinHandleCell.js +58 -0
  111. package/Table/components/TableExpandedRow.js +11 -2
  112. package/Table/components/TableHeader.js +12 -10
  113. package/Table/components/TableRow.js +38 -13
  114. package/Table/components/TableSelectionCell.js +1 -1
  115. package/Table/components/TableToggleableCell.d.ts +16 -0
  116. package/Table/components/TableToggleableCell.js +51 -0
  117. package/Table/components/index.d.ts +4 -1
  118. package/Table/components/index.js +3 -0
  119. package/Table/hooks/typings.d.ts +18 -4
  120. package/Table/hooks/useTableExpansion.d.ts +2 -2
  121. package/Table/hooks/useTableExpansion.js +5 -5
  122. package/Table/hooks/useTableFixedOffsets.d.ts +6 -2
  123. package/Table/hooks/useTableFixedOffsets.js +58 -24
  124. package/Table/hooks/useTableScroll.d.ts +9 -3
  125. package/Table/hooks/useTableScroll.js +34 -7
  126. package/Table/hooks/useTableVirtualization.d.ts +2 -1
  127. package/Table/hooks/useTableVirtualization.js +2 -8
  128. package/Table/index.d.ts +4 -3
  129. package/Table/index.js +3 -0
  130. package/Table/typings.d.ts +172 -0
  131. package/Transition/Slide.d.ts +9 -2
  132. package/Transition/Slide.js +7 -4
  133. package/Tree/TreeNode.js +1 -1
  134. package/index.d.ts +4 -2
  135. package/index.js +6 -3
  136. package/package.json +6 -4
  137. package/Navigation/CollapsedMenu.d.ts +0 -6
  138. package/Navigation/CollapsedMenu.js +0 -16
  139. package/PageToolbar/PageToolbar.d.ts +0 -110
  140. package/PageToolbar/PageToolbar.js +0 -23
  141. package/PageToolbar/index.d.ts +0 -2
  142. package/PageToolbar/index.js +0 -1
  143. package/PageToolbar/utils.d.ts +0 -23
  144. package/PageToolbar/utils.js +0 -157
  145. package/Table/components/TableDragHandleCell.d.ts +0 -11
  146. package/Table/components/TableDragHandleCell.js +0 -44
@@ -0,0 +1,58 @@
1
+ import { ComponentPropsWithoutRef, ReactElement } from 'react';
2
+ import { FilterAreaActionsAlign, FilterAreaSize } from '@mezzanine-ui/core/filter-area';
3
+ import { NativeElementPropsWithoutKeyAndRef } from '../utils/jsx-types';
4
+ import { FilterLineProps } from './FilterLine';
5
+ export interface FilterAreaProps extends Omit<NativeElementPropsWithoutKeyAndRef<'div'>, 'children' | 'onSubmit' | 'onReset'> {
6
+ /**
7
+ * The alignment of the actions.
8
+ * @default 'end'
9
+ */
10
+ actionsAlign?: FilterAreaActionsAlign;
11
+ /**
12
+ * The content of the filter area, must be FilterLine component(s).
13
+ */
14
+ children: ReactElement<FilterLineProps> | ReactElement<FilterLineProps>[];
15
+ /**
16
+ * Whether the form has been modified from its initial state.
17
+ * When false, the reset button will be disabled.
18
+ * @default true
19
+ */
20
+ isDirty?: boolean;
21
+ /**
22
+ * Callback function triggered when the form is reset.
23
+ * Used to clear all filter conditions and restore to initial state.
24
+ */
25
+ onReset?: () => void;
26
+ /**
27
+ * Callback function triggered when the form is submitted.
28
+ * FilterArea itself does not manage form state; the parent component should collect
29
+ * filter values and handle submission logic. If using react-hook-form, values will be
30
+ * handled through FormProvider's handleSubmit.
31
+ */
32
+ onSubmit?: () => void;
33
+ /**
34
+ * The text of the reset button.
35
+ */
36
+ resetText?: string;
37
+ /**
38
+ * The size of the filter area.
39
+ * @default 'main'
40
+ */
41
+ size?: FilterAreaSize;
42
+ /**
43
+ * The text of the submit button.
44
+ */
45
+ submitText?: string;
46
+ /**
47
+ * The type of the reset button.
48
+ * @default 'button'
49
+ */
50
+ resetButtonType?: ComponentPropsWithoutRef<'button'>['type'];
51
+ /**
52
+ * The type of the submit button.
53
+ * @default 'button'
54
+ */
55
+ submitButtonType?: ComponentPropsWithoutRef<'button'>['type'];
56
+ }
57
+ declare const FilterArea: import("react").ForwardRefExoticComponent<FilterAreaProps & import("react").RefAttributes<HTMLDivElement>>;
58
+ export default FilterArea;
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { forwardRef, useMemo, Children, useState, useCallback } from 'react';
4
+ import { filterAreaClasses } from '@mezzanine-ui/core/filter-area';
5
+ import { ChevronUpIcon, ChevronDownIcon } from '@mezzanine-ui/icons';
6
+ import Button from '../Button/Button.js';
7
+ import cx from 'clsx';
8
+
9
+ const FilterArea = forwardRef(function FilterArea(props, ref) {
10
+ const { actionsAlign = 'end', children, className, isDirty = true, onReset, onSubmit, resetText = 'Reset', size = 'main', submitText = 'Search', resetButtonType = 'button', submitButtonType = 'button', ...rest } = props;
11
+ const filterLines = useMemo(() => Children.toArray(children), [children]);
12
+ const hasMultipleLines = filterLines.length > 1;
13
+ const [expanded, setExpanded] = useState(false);
14
+ const handleToggleExpanded = useCallback(() => {
15
+ setExpanded((prev) => !prev);
16
+ }, []);
17
+ const handleSubmit = useCallback(() => {
18
+ if (onSubmit) {
19
+ onSubmit();
20
+ }
21
+ }, [onSubmit]);
22
+ const renderAction = () => {
23
+ return (jsxs("div", { className: cx(filterAreaClasses.actions, filterAreaClasses.actionsAlign(actionsAlign), { [filterAreaClasses.actionsExpanded]: expanded }), children: [jsx(Button, { onClick: handleSubmit, size: size, type: submitButtonType, children: submitText }), jsx(Button, { disabled: !isDirty, onClick: onReset, size: size, type: resetButtonType, variant: "base-secondary", children: resetText }), hasMultipleLines && (jsx(Button, { "aria-expanded": expanded, "aria-label": expanded ? 'Collapse filters' : 'Expand filters', icon: expanded ? ChevronUpIcon : ChevronDownIcon, iconType: "icon-only", onClick: handleToggleExpanded, size: size, title: expanded ? 'Collapse filters' : 'Expand filters', type: "button", variant: "base-ghost" }))] }));
24
+ };
25
+ const firstLine = filterLines[0];
26
+ return (jsx("div", { ...rest, ref: ref, className: cx(filterAreaClasses.host, className, {
27
+ [filterAreaClasses.size(size)]: size,
28
+ }), children: expanded ? (jsxs(Fragment, { children: [filterLines, renderAction()] })) : (jsx(Fragment, { children: firstLine && (jsxs("div", { className: filterAreaClasses.row, children: [firstLine, renderAction()] })) })) }));
29
+ });
30
+
31
+ export { FilterArea as default };
@@ -0,0 +1,11 @@
1
+ import { ReactElement } from 'react';
2
+ import { NativeElementPropsWithoutKeyAndRef } from '../utils/jsx-types';
3
+ import { FilterProps } from './Filter';
4
+ export interface FilterLineProps extends Omit<NativeElementPropsWithoutKeyAndRef<'div'>, 'children'> {
5
+ /**
6
+ * The content of the filter line, must be Filter component(s).
7
+ */
8
+ children: ReactElement<FilterProps> | ReactElement<FilterProps>[];
9
+ }
10
+ declare const FilterLine: import("react").ForwardRefExoticComponent<FilterLineProps & import("react").RefAttributes<HTMLDivElement>>;
11
+ export default FilterLine;
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { forwardRef, useMemo } from 'react';
4
+ import { filterAreaClasses } from '@mezzanine-ui/core/filter-area';
5
+ import cx from 'clsx';
6
+
7
+ const FilterLine = forwardRef(function FilterLine(props, ref) {
8
+ const { className, children, ...rest } = props;
9
+ const lineClassName = useMemo(() => cx(filterAreaClasses.line, className), [className]);
10
+ return (jsx("div", { ...rest, ref: ref, className: lineClassName, children: children }));
11
+ });
12
+
13
+ export { FilterLine as default };
@@ -0,0 +1,6 @@
1
+ export { default as FilterArea } from './FilterArea';
2
+ export type { FilterAreaProps } from './FilterArea';
3
+ export { default as FilterLine } from './FilterLine';
4
+ export type { FilterLineProps } from './FilterLine';
5
+ export { default as Filter } from './Filter';
6
+ export type { FilterProps } from './Filter';
@@ -0,0 +1,3 @@
1
+ export { default as FilterArea } from './FilterArea.js';
2
+ export { default as FilterLine } from './FilterLine.js';
3
+ export { default as Filter } from './Filter.js';
@@ -2,19 +2,18 @@ import { MouseEvent } from 'react';
2
2
  import { SelectValue } from '../Select/typings';
3
3
  export interface UseSelectBaseValueControl {
4
4
  onClear?(e: MouseEvent<Element>): void;
5
- onChange?(newOptions: SelectValue[] | SelectValue): any;
6
5
  onClose?(): void;
7
6
  }
8
7
  export type UseSelectMultipleValueControl = UseSelectBaseValueControl & {
9
8
  defaultValue?: SelectValue[];
10
9
  mode: 'multiple';
11
- onChange?(newOptions: SelectValue[]): any;
10
+ onChange?(newOptions: SelectValue[]): void;
12
11
  value?: SelectValue[];
13
12
  };
14
13
  export type UseSelectSingleValueControl = UseSelectBaseValueControl & {
15
14
  defaultValue?: SelectValue;
16
15
  mode: 'single';
17
- onChange?(newOption: SelectValue): any;
16
+ onChange?(newOption: SelectValue | null): void;
18
17
  value?: SelectValue | null;
19
18
  };
20
19
  export type UseSelectValueControl = UseSelectMultipleValueControl | UseSelectSingleValueControl;
@@ -22,7 +21,7 @@ export interface SelectBaseValueControl {
22
21
  onClear(e: MouseEvent<Element>): void;
23
22
  }
24
23
  export type SelectMultipleValueControl = SelectBaseValueControl & {
25
- onChange: (v: SelectValue | null) => SelectValue[];
24
+ onChange: (v: SelectValue | SelectValue[] | null) => SelectValue[];
26
25
  value: SelectValue[];
27
26
  };
28
27
  export type SelectSingleValueControl = SelectBaseValueControl & {
@@ -9,55 +9,67 @@ function useSelectBaseValueControl(props) {
9
9
  equalityFn,
10
10
  value: valueProp,
11
11
  });
12
- return {
13
- value,
14
- onChange: (chooseOption) => {
12
+ if (mode === 'multiple') {
13
+ const onChangeMultiple = (chooseOption) => {
15
14
  var _a;
16
15
  if (!chooseOption) {
17
- if (mode === 'multiple') {
18
- return [];
19
- }
20
- return null;
16
+ return [];
21
17
  }
22
- let newValue = mode === 'multiple' ? [] : null;
23
- switch (mode) {
24
- case 'multiple': {
25
- const existedValueIdx = ((_a = value) !== null && _a !== void 0 ? _a : []).findIndex((v) => v.id === chooseOption.id);
26
- if (~existedValueIdx) {
27
- newValue = [
28
- ...value.slice(0, existedValueIdx),
29
- ...value.slice(existedValueIdx + 1),
30
- ];
31
- }
32
- else {
33
- newValue = [...value, chooseOption];
34
- }
35
- if (typeof onChange === 'function')
36
- onChange(newValue);
37
- break;
18
+ let newValue = [];
19
+ if (Array.isArray(chooseOption)) {
20
+ newValue = chooseOption;
21
+ }
22
+ else {
23
+ const existedValueIdx = ((_a = value) !== null && _a !== void 0 ? _a : []).findIndex((v) => v.id === chooseOption.id);
24
+ if (~existedValueIdx) {
25
+ newValue = [
26
+ ...value.slice(0, existedValueIdx),
27
+ ...value.slice(existedValueIdx + 1),
28
+ ];
38
29
  }
39
- default: {
40
- newValue = chooseOption;
41
- if (typeof onClose === 'function') {
42
- /** single selection should close modal when clicked */
43
- onClose();
44
- }
45
- if (typeof onChange === 'function')
46
- onChange(newValue);
47
- break;
30
+ else {
31
+ newValue = [...value, chooseOption];
48
32
  }
49
33
  }
34
+ if (typeof onChange === 'function')
35
+ onChange(newValue);
50
36
  setValue(newValue);
51
37
  return newValue;
52
- },
38
+ };
39
+ return {
40
+ value: value,
41
+ onChange: onChangeMultiple,
42
+ onClear: (e) => {
43
+ e.stopPropagation();
44
+ setValue([]);
45
+ onChange === null || onChange === void 0 ? void 0 : onChange([]);
46
+ if (typeof onClearProp === 'function') {
47
+ onClearProp(e);
48
+ }
49
+ },
50
+ };
51
+ }
52
+ const onChangeSingle = (chooseOption) => {
53
+ if (!chooseOption) {
54
+ return null;
55
+ }
56
+ const newValue = chooseOption;
57
+ if (typeof onClose === 'function') {
58
+ /** single selection should close modal when clicked */
59
+ onClose();
60
+ }
61
+ if (typeof onChange === 'function')
62
+ onChange(newValue);
63
+ setValue(newValue);
64
+ return newValue;
65
+ };
66
+ return {
67
+ value: value,
68
+ onChange: onChangeSingle,
53
69
  onClear: (e) => {
54
70
  e.stopPropagation();
55
- if (mode === 'multiple') {
56
- setValue([]);
57
- }
58
- else {
59
- setValue(null);
60
- }
71
+ setValue(null);
72
+ onChange === null || onChange === void 0 ? void 0 : onChange(null);
61
73
  if (typeof onClearProp === 'function') {
62
74
  onClearProp(e);
63
75
  }
package/Input/Input.d.ts CHANGED
@@ -17,6 +17,7 @@ export interface InputBaseProps extends Omit<TextFieldProps, 'children' | 'clear
17
17
  /**
18
18
  * Formatter function to transform the value for display.
19
19
  * Common use cases: currency formatting (1000 → "1,000"), phone numbers, etc.
20
+ * Default to formatting number with commas for "currency" variant.
20
21
  * @example
21
22
  * formatter={(value) => value.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
22
23
  */
@@ -49,6 +50,7 @@ export interface InputBaseProps extends Omit<TextFieldProps, 'children' | 'clear
49
50
  /**
50
51
  * Parser function to extract the raw value from formatted display value.
51
52
  * Should reverse the formatter transformation.
53
+ * Default to removing commas for "currency" formatting.
52
54
  * @example
53
55
  * parser={(value) => value.replace(/,/g, '')}
54
56
  */
@@ -113,10 +115,10 @@ export type NumberInputProps = InputBaseProps & NumberInput & {
113
115
  variant: 'number';
114
116
  };
115
117
  /**
116
- * 5. Unit Input - Input with unit text and spinner buttons
118
+ * 5. Currency Input - Input with unit text and spinner buttons
117
119
  */
118
- export type UnitInputProps = InputBaseProps & NumberInput & TextFieldAffixProps & {
119
- variant: 'unit';
120
+ export type CurrencyInputProps = InputBaseProps & NumberInput & TextFieldAffixProps & {
121
+ variant: 'currency';
120
122
  /**
121
123
  * Whether to show spinner buttons.
122
124
  * @default false
@@ -201,7 +203,7 @@ export type WithPasswordStrengthIndicator = {
201
203
  export type PasswordInputProps = InputBaseProps & ClearableInput & WithPasswordStrengthIndicator & {
202
204
  variant: 'password';
203
205
  };
204
- export type InputProps = BaseInputProps | WithAffixInputProps | SearchInputProps | NumberInputProps | UnitInputProps | ActionInputProps | SelectInputProps | PasswordInputProps;
206
+ export type InputProps = BaseInputProps | WithAffixInputProps | SearchInputProps | NumberInputProps | CurrencyInputProps | ActionInputProps | SelectInputProps | PasswordInputProps;
205
207
  /**
206
208
  * The react component for `mezzanine` input.
207
209
  */
package/Input/Input.js CHANGED
@@ -2,9 +2,11 @@
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import { inputClasses } from '@mezzanine-ui/core/input';
4
4
  import { EyeIcon, EyeInvisibleIcon, SearchIcon } from '@mezzanine-ui/icons';
5
- import { forwardRef, useRef, useState, useCallback } from 'react';
5
+ import { forwardRef, useRef, useState, useMemo, useCallback } from 'react';
6
6
  import { useInputWithClearControlValue } from '../Form/useInputWithClearControlValue.js';
7
7
  import { useComposeRefs } from '../hooks/useComposeRefs.js';
8
+ import { formatNumberWithCommas } from '../utils/format-number-with-commas.js';
9
+ import { parseNumberWithCommas } from '../utils/parse-number-with-commas.js';
8
10
  import Icon from '../Icon/Icon.js';
9
11
  import Dropdown from '../Dropdown/Dropdown.js';
10
12
  import SelectButton from './SelectButton/SelectButton.js';
@@ -18,7 +20,7 @@ import cx from 'clsx';
18
20
  * The react component for `mezzanine` input.
19
21
  */
20
22
  const Input = forwardRef(function Input(props, ref) {
21
- const { active, className, defaultValue, disabled = false, error = false, formatter, fullWidth = true, id, inputProps, inputType, inputRef: inputRefProp, name, onChange: onChangeProp, parser, placeholder, readonly, size = 'main', typing, variant = 'base', value: valueProp, } = props;
23
+ const { active, className, defaultValue, disabled = false, error = false, formatter: formatterProp, fullWidth = true, id, inputProps, inputType, inputRef: inputRefProp, name, onChange: onChangeProp, parser: parserProp, placeholder, readonly, size = 'main', typing, variant = 'base', value: valueProp, } = props;
22
24
  const inputRef = useRef(null);
23
25
  const [showPassword, setShowPassword] = useState(false);
24
26
  const [value, onChange, onClearFromHook] = useInputWithClearControlValue({
@@ -28,6 +30,22 @@ const Input = forwardRef(function Input(props, ref) {
28
30
  value: valueProp,
29
31
  });
30
32
  // Handle formatter/parser logic
33
+ const formatter = useMemo(() => {
34
+ if (formatterProp)
35
+ return formatterProp;
36
+ if (variant === 'currency') {
37
+ return (value) => formatNumberWithCommas(value);
38
+ }
39
+ return undefined;
40
+ }, [formatterProp, variant]);
41
+ const parser = useMemo(() => {
42
+ if (parserProp)
43
+ return parserProp;
44
+ if (variant === 'currency') {
45
+ return (value) => { var _a, _b; return (_b = (_a = parseNumberWithCommas(value)) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : ''; };
46
+ }
47
+ return undefined;
48
+ }, [parserProp, variant]);
31
49
  const handleChange = useCallback((event) => {
32
50
  let newValue = event.target.value;
33
51
  // Parse the formatted value back to raw value if parser is provided
@@ -106,20 +124,20 @@ const Input = forwardRef(function Input(props, ref) {
106
124
  };
107
125
  break;
108
126
  }
109
- case 'unit': {
110
- const unitProps = props;
111
- const { step = 1, max, min, onSpinUp, onSpinDown } = unitProps;
127
+ case 'currency': {
128
+ const currencyProps = props;
129
+ const { step = 1, max, min, onSpinUp, onSpinDown } = currencyProps;
112
130
  // 預設置右對齊
113
131
  inputStyle = { textAlign: 'right' };
114
132
  // 允許填入 prefix/suffix
115
- prefix = unitProps.prefix;
116
- suffix = unitProps.suffix;
133
+ prefix = currencyProps.prefix;
134
+ suffix = currencyProps.suffix;
117
135
  defaultInputProps = {
118
136
  min: min,
119
137
  max: max,
120
138
  step: step,
121
139
  };
122
- if (unitProps.showSpinner) {
140
+ if (currencyProps.showSpinner) {
123
141
  const handleSpinUp = () => {
124
142
  const currentValue = parseFloat(value || '0');
125
143
  const newValue = currentValue + step;
@@ -140,7 +158,7 @@ const Input = forwardRef(function Input(props, ref) {
140
158
  }
141
159
  onSpinDown === null || onSpinDown === void 0 ? void 0 : onSpinDown();
142
160
  };
143
- suffix = (jsxs(Fragment, { children: [unitProps.suffix, jsxs("div", { className: inputClasses.spinners, children: [jsx(SpinnerButton, { type: "up", size: size, disabled: disabled, onClick: handleSpinUp }), jsx(SpinnerButton, { type: "down", size: size, disabled: disabled, onClick: handleSpinDown })] })] }));
161
+ suffix = (jsxs(Fragment, { children: [currencyProps.suffix, jsxs("div", { className: inputClasses.spinners, children: [jsx(SpinnerButton, { type: "up", size: size, disabled: disabled, onClick: handleSpinUp }), jsx(SpinnerButton, { type: "down", size: size, disabled: disabled, onClick: handleSpinDown })] })] }));
144
162
  }
145
163
  break;
146
164
  }
@@ -159,7 +177,7 @@ const Input = forwardRef(function Input(props, ref) {
159
177
  }
160
178
  case 'select': {
161
179
  const selectProps = props;
162
- const { selectButton, options, dropdownWidth = 120, dropdownMaxHeight = 114 } = selectProps;
180
+ const { selectButton, options, dropdownWidth = 120, dropdownMaxHeight = 114, } = selectProps;
163
181
  const defaultOptions = options || [];
164
182
  const selectedOptions = defaultOptions.length > 0
165
183
  ? defaultOptions.map((option) => ({
package/Input/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export type { InputSize, InputStrength } from '@mezzanine-ui/core/input';
2
- export type { InputBaseProps, ClearableInput, NumberInput, BaseInputProps, WithAffixInputProps, SearchInputProps, NumberInputProps, UnitInputProps, ActionInputProps, SelectInputProps, WithPasswordStrengthIndicator, PasswordInputProps, InputProps, } from './Input';
2
+ export type { InputBaseProps, ClearableInput, NumberInput, BaseInputProps, WithAffixInputProps, SearchInputProps, NumberInputProps, CurrencyInputProps, ActionInputProps, SelectInputProps, WithPasswordStrengthIndicator, PasswordInputProps, InputProps, } from './Input';
3
3
  export { default } from './Input';
@@ -0,0 +1,54 @@
1
+ import { NativeElementPropsWithoutKeyAndRef } from '../utils/jsx-types';
2
+ import { ModalContainerProps } from './useModalContainer';
3
+ export interface MediaPreviewModalProps extends Omit<ModalContainerProps, 'children'>, NativeElementPropsWithoutKeyAndRef<'div'> {
4
+ /**
5
+ * The current index of the media being displayed (controlled mode).
6
+ * If provided along with onNext/onPrev, the component operates in controlled mode.
7
+ */
8
+ currentIndex?: number;
9
+ /**
10
+ * The default index when the modal opens (uncontrolled mode).
11
+ * @default 0
12
+ */
13
+ defaultIndex?: number;
14
+ /**
15
+ * Whether to disable the next navigation button.
16
+ * @default false
17
+ */
18
+ disableNext?: boolean;
19
+ /**
20
+ * Whether to disable the previous navigation button.
21
+ * @default false
22
+ */
23
+ disablePrev?: boolean;
24
+ /**
25
+ * Array of media items to display.
26
+ * Each item should be a valid image URL or React node.
27
+ */
28
+ mediaItems: (string | React.ReactNode)[];
29
+ /**
30
+ * Callback fired when the index changes (uncontrolled mode).
31
+ */
32
+ onIndexChange?: (index: number) => void;
33
+ /**
34
+ * Callback fired when the next navigation button is clicked (controlled mode).
35
+ * If provided, the component operates in controlled mode.
36
+ */
37
+ onNext?: () => void;
38
+ /**
39
+ * Callback fired when the previous navigation button is clicked (controlled mode).
40
+ * If provided, the component operates in controlled mode.
41
+ */
42
+ onPrev?: () => void;
43
+ /**
44
+ * Whether to show the pagination indicator.
45
+ * @default true
46
+ */
47
+ showPaginationIndicator?: boolean;
48
+ }
49
+ /**
50
+ * The react component for `mezzanine` media preview modal.
51
+ * Displays media items with navigation controls and a close button.
52
+ */
53
+ declare const MediaPreviewModal: import("react").ForwardRefExoticComponent<MediaPreviewModalProps & import("react").RefAttributes<HTMLDivElement>>;
54
+ export default MediaPreviewModal;
@@ -0,0 +1,158 @@
1
+ 'use client';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import { forwardRef, useState, useEffect, useRef, useMemo } from 'react';
4
+ import { ChevronLeftIcon, ChevronRightIcon } from '@mezzanine-ui/icons';
5
+ import { modalClasses } from '@mezzanine-ui/core/modal';
6
+ import { MOTION_DURATION, MOTION_EASING } from '@mezzanine-ui/system/motion';
7
+ import useModalContainer from './useModalContainer.js';
8
+ import ClearActions from '../ClearActions/ClearActions.js';
9
+ import Icon from '../Icon/Icon.js';
10
+ import Fade from '../Transition/Fade.js';
11
+ import cx from 'clsx';
12
+
13
+ /**
14
+ * The react component for `mezzanine` media preview modal.
15
+ * Displays media items with navigation controls and a close button.
16
+ */
17
+ const MediaPreviewModal = forwardRef(function MediaPreviewModal(props, ref) {
18
+ const { className, container, currentIndex: controlledIndex, defaultIndex = 0, disableCloseOnBackdropClick = false, disableCloseOnEscapeKeyDown = false, disableNext = false, disablePortal = false, disablePrev = false, mediaItems, onBackdropClick, onClose, onIndexChange, onNext, onPrev, open, showPaginationIndicator = true, ...rest } = props;
19
+ const { Container: ModalContainer } = useModalContainer();
20
+ // Determine if component is in controlled mode
21
+ const isControlled = controlledIndex !== undefined;
22
+ // Internal state for uncontrolled mode
23
+ const [internalIndex, setInternalIndex] = useState(defaultIndex);
24
+ // Use controlled index if provided, otherwise use internal state
25
+ const currentIndex = isControlled ? controlledIndex : internalIndex;
26
+ // Reset internal index when modal opens in uncontrolled mode
27
+ useEffect(() => {
28
+ if (open && !isControlled) {
29
+ setInternalIndex(defaultIndex);
30
+ }
31
+ }, [open, isControlled, defaultIndex]);
32
+ // Built-in navigation handlers for uncontrolled mode
33
+ const handleNext = () => {
34
+ if (onNext) {
35
+ // Controlled mode: use provided callback
36
+ onNext();
37
+ }
38
+ else {
39
+ // Uncontrolled mode: update internal state
40
+ const nextIndex = Math.min(currentIndex + 1, mediaItems.length - 1);
41
+ setInternalIndex(nextIndex);
42
+ onIndexChange === null || onIndexChange === void 0 ? void 0 : onIndexChange(nextIndex);
43
+ }
44
+ };
45
+ const handlePrev = () => {
46
+ if (onPrev) {
47
+ // Controlled mode: use provided callback
48
+ onPrev();
49
+ }
50
+ else {
51
+ // Uncontrolled mode: update internal state
52
+ const prevIndex = Math.max(currentIndex - 1, 0);
53
+ setInternalIndex(prevIndex);
54
+ onIndexChange === null || onIndexChange === void 0 ? void 0 : onIndexChange(prevIndex);
55
+ }
56
+ };
57
+ // Auto-calculate disable states for uncontrolled mode
58
+ const isNextDisabled = disableNext || currentIndex >= mediaItems.length - 1;
59
+ const isPrevDisabled = disablePrev || currentIndex <= 0;
60
+ const [displayedIndices, setDisplayedIndices] = useState([
61
+ currentIndex,
62
+ ]);
63
+ const [activeIndex, setActiveIndex] = useState(currentIndex);
64
+ const preloadedUrls = useRef(new Set());
65
+ // Helper function to preload a single image
66
+ const preloadImage = (url) => {
67
+ if (preloadedUrls.current.has(url))
68
+ return;
69
+ const img = new Image();
70
+ img.src = url;
71
+ preloadedUrls.current.add(url);
72
+ };
73
+ // Preload images: prioritize current and adjacent, then load others
74
+ useEffect(() => {
75
+ if (!open)
76
+ return;
77
+ // Priority 1: Current and adjacent images
78
+ const priorityIndices = [
79
+ currentIndex - 1,
80
+ currentIndex,
81
+ currentIndex + 1,
82
+ ].filter((idx) => idx >= 0 && idx < mediaItems.length);
83
+ priorityIndices.forEach((idx) => {
84
+ const media = mediaItems[idx];
85
+ if (typeof media === 'string') {
86
+ preloadImage(media);
87
+ }
88
+ });
89
+ // Priority 2: All other images (load after a short delay)
90
+ const loadRemainingTimer = setTimeout(() => {
91
+ mediaItems.forEach((media, idx) => {
92
+ if (typeof media === 'string' && !priorityIndices.includes(idx)) {
93
+ preloadImage(media);
94
+ }
95
+ });
96
+ }, 500);
97
+ return () => clearTimeout(loadRemainingTimer);
98
+ }, [open, currentIndex, mediaItems]);
99
+ useEffect(() => {
100
+ if (currentIndex !== activeIndex) {
101
+ // First, add new index to displayedIndices (will render with in=false)
102
+ setDisplayedIndices((prev) => prev.includes(currentIndex) ? prev : [...prev, currentIndex]);
103
+ // Use requestAnimationFrame to ensure DOM is updated before triggering animation
104
+ let rafId1 = null;
105
+ let rafId2 = null;
106
+ rafId1 = requestAnimationFrame(() => {
107
+ rafId2 = requestAnimationFrame(() => {
108
+ setActiveIndex(currentIndex);
109
+ });
110
+ });
111
+ // Clean up old images after transition completes
112
+ const cleanupTimer = setTimeout(() => {
113
+ setDisplayedIndices([currentIndex]);
114
+ }, MOTION_DURATION.fast + 100);
115
+ return () => {
116
+ clearTimeout(cleanupTimer);
117
+ if (rafId1 !== null) {
118
+ cancelAnimationFrame(rafId1);
119
+ }
120
+ if (rafId2 !== null) {
121
+ cancelAnimationFrame(rafId2);
122
+ }
123
+ };
124
+ }
125
+ return undefined;
126
+ }, [currentIndex, activeIndex]);
127
+ // Memoize media elements to prevent unnecessary re-renders
128
+ const mediaElements = useMemo(() => {
129
+ return mediaItems.map((media, index) => {
130
+ if (typeof media === 'string') {
131
+ return {
132
+ index,
133
+ element: (jsx("img", { alt: `Media ${index + 1}`, className: modalClasses.mediaPreviewImage, src: media })),
134
+ };
135
+ }
136
+ return {
137
+ index,
138
+ element: jsx("div", { className: modalClasses.mediaPreviewImage, children: media }),
139
+ };
140
+ });
141
+ }, [mediaItems]);
142
+ const renderMedia = (index) => {
143
+ const mediaElement = mediaElements.find((item) => item.index === index);
144
+ if (!mediaElement)
145
+ return null;
146
+ const isCurrent = index === activeIndex;
147
+ return (jsx(Fade, { duration: {
148
+ enter: MOTION_DURATION.fast,
149
+ exit: MOTION_DURATION.fast,
150
+ }, easing: {
151
+ enter: MOTION_EASING.entrance,
152
+ exit: MOTION_EASING.exit,
153
+ }, in: isCurrent, children: mediaElement.element }, index));
154
+ };
155
+ return (jsxs(ModalContainer, { className: modalClasses.overlay, container: container, disableCloseOnBackdropClick: disableCloseOnBackdropClick, disableCloseOnEscapeKeyDown: disableCloseOnEscapeKeyDown, disablePortal: disablePortal, onBackdropClick: onBackdropClick, onClose: onClose, open: open, ref: ref, children: [jsx("div", { ...rest, className: cx(modalClasses.host, modalClasses.mediaPreview, className), role: "dialog", children: jsx("div", { className: modalClasses.mediaPreviewContent, children: jsx("div", { className: modalClasses.mediaPreviewMediaContainer, children: displayedIndices.map((index) => renderMedia(index)) }) }) }), jsx(ClearActions, { className: modalClasses.mediaPreviewCloseButton, onClick: onClose, type: "embedded", variant: "contrast" }), mediaItems.length > 1 && (jsx("button", { "aria-disabled": isPrevDisabled, "aria-label": "Previous media", className: cx(modalClasses.mediaPreviewNavButton, modalClasses.mediaPreviewNavButtonPrev), disabled: isPrevDisabled, onClick: handlePrev, title: "Previous", type: "button", children: jsx(Icon, { icon: ChevronLeftIcon, size: 16, color: "fixed-light" }) })), mediaItems.length > 1 && (jsx("button", { "aria-disabled": isNextDisabled, "aria-label": "Next media", className: cx(modalClasses.mediaPreviewNavButton, modalClasses.mediaPreviewNavButtonNext), disabled: isNextDisabled, onClick: handleNext, title: "Next", type: "button", children: jsx(Icon, { icon: ChevronRightIcon, size: 16, color: "fixed-light" }) })), showPaginationIndicator && mediaItems.length > 1 && (jsxs("div", { "aria-label": `Page ${currentIndex + 1} of ${mediaItems.length}`, className: modalClasses.mediaPreviewPaginationIndicator, children: [currentIndex + 1, "/", mediaItems.length] }))] }));
156
+ });
157
+
158
+ export { MediaPreviewModal as default };
package/Modal/Modal.js CHANGED
@@ -25,7 +25,7 @@ const Modal = forwardRef(function Modal(props, ref) {
25
25
  }, className), role: "dialog", children: [showModalHeader && (jsx(ModalHeader, { showStatusTypeIcon: showStatusTypeIcon, statusTypeIconLayout: statusTypeIconLayout, supportingText: supportingText, supportingTextAlign: supportingTextAlign, title: title, titleAlign: titleAlign })), modalType === 'extendedSplit' && (jsxs("div", { className: modalClasses.modalBodyContainerExtendedSplit, children: [jsx("div", { className: modalClasses.modalBodyContainerExtendedSplitRight, children: extendedSplitRightSideContent }), jsxs("div", { className: modalClasses.modalBodyContainerExtendedSplitLeft, children: [jsx("div", { className: modalClasses.modalBodyContainerExtendedSplitLeftSideContent, children: extendedSplitLeftSideContent }), showModalFooter && renderModalFooter()] })] })), (modalType === 'standard' ||
26
26
  modalType === 'verification' ||
27
27
  modalType === 'extended' ||
28
- modalType === 'mediaPreview') && (jsxs(Fragment, { children: [jsx("div", { className: modalClasses.modalBodyContainer, children: children }), showModalFooter && renderModalFooter()] })), showDismissButton && (jsx(ClearActions, { onClick: onClose, className: modalClasses.closeIcon, variant: "base" }))] }) }) }));
28
+ modalType === 'mediaPreview') && (jsxs(Fragment, { children: [children && (jsx("div", { className: modalClasses.modalBodyContainer, children: children })), showModalFooter && renderModalFooter()] })), showDismissButton && (jsx(ClearActions, { onClick: onClose, className: modalClasses.closeIcon, variant: "base" }))] }) }) }));
29
29
  });
30
30
 
31
31
  export { Modal as default };
package/Modal/index.d.ts CHANGED
@@ -5,6 +5,8 @@ export type { ModalFooterProps } from './ModalFooter';
5
5
  export { default as ModalFooter } from './ModalFooter';
6
6
  export type { ModalHeaderProps } from './ModalHeader';
7
7
  export { default as ModalHeader } from './ModalHeader';
8
+ export type { MediaPreviewModalProps } from './MediaPreviewModal';
9
+ export { default as MediaPreviewModal } from './MediaPreviewModal';
8
10
  export { default as useModalContainer } from './useModalContainer';
9
11
  export type { ModalContainerProps } from './useModalContainer';
10
12
  export type { ModalProps } from './Modal';
package/Modal/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { default as ModalBodyForVerification } from './ModalBodyForVerification.js';
2
2
  export { default as ModalFooter } from './ModalFooter.js';
3
3
  export { default as ModalHeader } from './ModalHeader.js';
4
+ export { default as MediaPreviewModal } from './MediaPreviewModal.js';
4
5
  export { default as useModalContainer } from './useModalContainer.js';
5
6
  export { default } from './Modal.js';