@mezzanine-ui/react 1.0.0-rc.7 → 1.0.0

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.
@@ -5,7 +5,7 @@ import { PopperProps } from '../Popper';
5
5
  import type { SelectTriggerInputProps, SelectTriggerProps } from '../Select/typings';
6
6
  import { SelectValue } from '../Select/typings';
7
7
  import { PickRenameMulti } from '../utils/general';
8
- export interface AutoCompleteBaseProps extends Omit<SelectTriggerProps, 'active' | 'clearable' | 'forceHideSuffixActionIcon' | 'mode' | 'onClick' | 'onKeyDown' | 'onChange' | 'renderValue' | 'inputProps' | 'suffixActionIcon' | 'value'>, PickRenameMulti<Pick<PopperProps, 'options'>, {
8
+ export interface AutoCompleteBaseProps extends Omit<SelectTriggerProps, 'active' | 'clearable' | 'forceHideSuffixActionIcon' | 'fullWidth' | 'mode' | 'onClick' | 'onKeyDown' | 'onChange' | 'renderValue' | 'inputProps' | 'suffixActionIcon' | 'value'>, PickRenameMulti<Pick<PopperProps, 'options'>, {
9
9
  options: 'popperOptions';
10
10
  }> {
11
11
  /**
@@ -7,11 +7,11 @@ import { useAutoCompleteValueControl } from '../Form/useAutoCompleteValueControl
7
7
  import { useComposeRefs } from '../hooks/useComposeRefs.js';
8
8
  import { SelectControlContext } from '../Select/SelectControlContext.js';
9
9
  import SelectTrigger from '../Select/SelectTrigger.js';
10
+ import AutoCompleteInsideTrigger from './AutoCompleteInside.js';
10
11
  import { useAutoCompleteCreation, getFullParsedList } from './useAutoCompleteCreation.js';
11
12
  import { useAutoCompleteKeyboard } from './useAutoCompleteKeyboard.js';
12
13
  import { useAutoCompleteSearch } from './useAutoCompleteSearch.js';
13
14
  import { useCreationTracker } from './useCreationTracker.js';
14
- import AutoCompleteInsideTrigger from './AutoCompleteInside.js';
15
15
  import { FormControlContext } from '../Form/FormControlContext.js';
16
16
  import Dropdown from '../Dropdown/Dropdown.js';
17
17
  import cx from 'clsx';
@@ -94,8 +94,8 @@ function isSingleValue(value) {
94
94
  * @see {@link useAutoCompleteValueControl} 管理 AutoComplete 內部值狀態的 hook
95
95
  */
96
96
  const AutoComplete = forwardRef(function AutoComplete(props, ref) {
97
- const { disabled: disabledFromFormControl, fullWidth: fullWidthFromFormControl, required: requiredFromFormControl, severity, } = useContext(FormControlContext) || {};
98
- const { addable = false, asyncData = false, className, clearSearchText = true, createSeparators = [',', '+', '\n'], defaultValue, disabled = disabledFromFormControl || false, disabledOptionsFilter = false, emptyText = '沒有符合的項目', error = severity === 'error' || false, fullWidth = fullWidthFromFormControl || false, id, inputPosition = 'outside', inputProps, inputRef, loading = false, loadingText = '載入中...', loadingPosition = 'bottom', menuMaxHeight, mode = 'single', name, onClear: onClearProp, onChange: onChangeProp, onInsert, onSearch, onSearchTextChange, onVisibilityChange, open: openProp, options: optionsProp, overflowStrategy, placeholder = '', prefix, required = requiredFromFormControl || false, searchDebounceTime = 300, searchTextControlRef, size, stepByStepBulkCreate = false, trimOnCreate = true, value: valueProp, createActionText, createActionTextTemplate = '建立 "{text}"', dropdownZIndex, globalPortal = true, onReachBottom, onLeaveBottom, onRemoveCreated, } = props;
97
+ const { disabled: disabledFromFormControl, required: requiredFromFormControl, severity, } = useContext(FormControlContext) || {};
98
+ const { addable = false, asyncData = false, className, clearSearchText = true, createSeparators = [',', '+', '\n'], defaultValue, disabled = disabledFromFormControl || false, disabledOptionsFilter = false, emptyText = '沒有符合的項目', error = severity === 'error' || false, id, inputPosition = 'outside', inputProps, inputRef, loading = false, loadingText = '載入中...', loadingPosition = 'bottom', menuMaxHeight, mode = 'single', name, onClear: onClearProp, onChange: onChangeProp, onInsert, onSearch, onSearchTextChange, onVisibilityChange, open: openProp, options: optionsProp, overflowStrategy, placeholder = '', prefix, required = requiredFromFormControl || false, searchDebounceTime = 300, searchTextControlRef, size, stepByStepBulkCreate = false, trimOnCreate = true, value: valueProp, createActionText, createActionTextTemplate = '建立 "{text}"', dropdownZIndex, globalPortal = true, onReachBottom, onLeaveBottom, onRemoveCreated, } = props;
99
99
  const shouldClearSearchTextOnBlur = clearSearchText;
100
100
  const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
101
101
  const isMultiple = mode === 'multiple';
@@ -676,20 +676,19 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
676
676
  role: 'combobox',
677
677
  };
678
678
  return (jsx(SelectControlContext.Provider, { value: context, children: jsx("div", { ref: nodeRef, className: cx(autocompleteClasses.host, {
679
- [autocompleteClasses.hostFullWidth]: fullWidth,
680
679
  [autocompleteClasses.hostInsideClosed]: inputPosition === 'inside' && !open,
681
680
  [autocompleteClasses.hostMode(mode)]: mode,
682
681
  }), children: jsx(Dropdown, { actionText: shouldShowCreateAction
683
682
  ? createActionText
684
683
  ? createActionText(createActionDisplayText)
685
684
  : createActionTextTemplate.replace('{text}', createActionDisplayText)
686
- : undefined, activeIndex: activeIndex, keyboardActiveIndex: keyboardActiveIndex, disabled: isInputDisabled, emptyText: emptyText, followText: searchText, inputPosition: inputPosition, isMatchInputValue: true, listboxId: menuId, loadingText: loadingText, loadingPosition: loadingPosition, maxHeight: menuMaxHeight, mode: mode, onActionCustom: shouldShowCreateAction ? handleActionCustom : undefined, onItemHover: setActiveIndex, onSelect: handleDropdownSelect, onVisibilityChange: handleVisibilityChange, open: open, options: asyncData && isLoading ? [] : dropdownOptions, placement: "bottom", sameWidth: true, showDropdownActions: shouldShowCreateAction, showActionShowTopBar: shouldShowCreateAction, status: dropdownStatus, toggleCheckedOnClick: inputPosition === 'inside' && mode === 'multiple' ? false : undefined, type: "default", value: dropdownValue, zIndex: dropdownZIndex, globalPortal: globalPortal, onReachBottom: onReachBottom, onLeaveBottom: onLeaveBottom, children: inputPosition === 'inside' ? (jsx(AutoCompleteInsideTrigger, { active: open, className: className, clearable: shouldForceClearable, disabled: isInputDisabled, error: error, fullWidth: fullWidth, inputRef: composedInputRef, onClear: handleClear, placeholder: getPlaceholder(), resolvedInputProps: {
685
+ : undefined, activeIndex: activeIndex, keyboardActiveIndex: keyboardActiveIndex, disabled: isInputDisabled, emptyText: emptyText, followText: searchText, inputPosition: inputPosition, isMatchInputValue: true, listboxId: menuId, loadingText: loadingText, loadingPosition: loadingPosition, maxHeight: menuMaxHeight, mode: mode, onActionCustom: shouldShowCreateAction ? handleActionCustom : undefined, onItemHover: setActiveIndex, onSelect: handleDropdownSelect, onVisibilityChange: handleVisibilityChange, open: open, options: asyncData && isLoading ? [] : dropdownOptions, placement: "bottom", sameWidth: true, showDropdownActions: shouldShowCreateAction, showActionShowTopBar: shouldShowCreateAction, status: dropdownStatus, toggleCheckedOnClick: inputPosition === 'inside' && mode === 'multiple' ? false : undefined, type: "default", value: dropdownValue, zIndex: dropdownZIndex, globalPortal: globalPortal, onReachBottom: onReachBottom, onLeaveBottom: onLeaveBottom, children: inputPosition === 'inside' ? (jsx(AutoCompleteInsideTrigger, { active: open, className: className, clearable: shouldForceClearable, disabled: isInputDisabled, error: error, inputRef: composedInputRef, onClear: handleClear, placeholder: getPlaceholder(), resolvedInputProps: {
687
686
  ...resolvedInputProps,
688
687
  onClick: (e) => {
689
688
  var _a;
690
689
  (_a = resolvedInputProps.onClick) === null || _a === void 0 ? void 0 : _a.call(resolvedInputProps, e);
691
690
  },
692
- }, size: size, value: insideInputValue })) : (jsx(SelectTrigger, { ref: composedRef, active: open, className: className, clearable: true, disabled: isInputDisabled, fullWidth: fullWidth, isForceClearable: shouldForceClearable, inputRef: composedInputRef, mode: mode, onTagClose: wrappedOnChange, onClear: handleClear, overflowStrategy: isMultiple ? (overflowStrategy !== null && overflowStrategy !== void 0 ? overflowStrategy : 'wrap') : overflowStrategy, placeholder: getPlaceholder(), prefix: prefix, readOnly: false, required: required, type: error ? 'error' : 'default', inputProps: {
691
+ }, size: size, value: insideInputValue })) : (jsx(SelectTrigger, { ref: composedRef, active: open, className: className, clearable: true, disabled: isInputDisabled, fullWidth: true, isForceClearable: shouldForceClearable, inputRef: composedInputRef, mode: mode, onTagClose: wrappedOnChange, onClear: handleClear, overflowStrategy: isMultiple ? (overflowStrategy !== null && overflowStrategy !== void 0 ? overflowStrategy : 'wrap') : overflowStrategy, placeholder: getPlaceholder(), prefix: prefix, readOnly: false, required: required, type: error ? 'error' : 'default', inputProps: {
693
692
  ...resolvedInputProps,
694
693
  onClick: (e) => {
695
694
  var _a;
@@ -26,10 +26,6 @@ export interface AutoCompleteInsideTriggerProps {
26
26
  * Input variant sizing.
27
27
  */
28
28
  size?: InputProps['size'];
29
- /**
30
- * Whether the input should occupy full width.
31
- */
32
- fullWidth?: boolean;
33
29
  /**
34
30
  * Additional class name for the trigger.
35
31
  */
@@ -7,9 +7,9 @@ function extractIdAndName(props) {
7
7
  return { id, name, readOnly, onChange, rest };
8
8
  }
9
9
  function AutoCompleteInsideTrigger(props) {
10
- const { active, className, clearable, disabled, error, fullWidth, inputRef, onClear, placeholder, resolvedInputProps, size, value, } = props;
10
+ const { active, className, clearable, disabled, error, inputRef, onClear, placeholder, resolvedInputProps, size, value, } = props;
11
11
  const { id, name, readOnly, onChange, rest } = extractIdAndName(resolvedInputProps);
12
- return (jsx(Input, { active: active, className: className, ...(disabled ? { disabled: true } : {}), error: error, fullWidth: fullWidth, id: id, name: name, placeholder: placeholder, readonly: readOnly || undefined, onChange: onChange, size: size, value: value, clearable: clearable, onClear: onClear,
12
+ return (jsx(Input, { active: active, className: className, ...(disabled ? { disabled: true } : {}), error: error, fullWidth: true, id: id, name: name, placeholder: placeholder, readonly: readOnly || undefined, onChange: onChange, size: size, value: value, clearable: clearable, onClear: onClear,
13
13
  // keep clear icon visibility behavior consistent with SelectTrigger
14
14
  forceShowClearable: clearable, inputRef: inputRef, inputProps: rest }));
15
15
  }
package/COMPONENTS.md CHANGED
@@ -4,18 +4,18 @@
4
4
 
5
5
  ## General(基礎)
6
6
 
7
- | 元件 | 匯入名稱 | 說明 |
8
- | ---------------- | ------------------ | ------------------------------------------------------------------------------------- |
9
- | Button | `Button` | 動作觸發按鈕,支援 primary / secondary / ghost / destructive / inverse 變體與多種尺寸 |
10
- | ButtonGroup | `ButtonGroup` | 將多個 Button 組合為群組,支援水平或垂直排列 |
11
- | Cropper | `Cropper` | 圖片裁切元件,支援自訂裁切區域與輸出尺寸 |
12
- | Icon | `Icon` | SVG 圖示元件,搭配 `@mezzanine-ui/icons` 使用,支援顏色與大小控制 |
13
- | Layout | `Layout` | 頁面整體佈局容器,提供左側面板、主要內容區、右側面板的三欄結構 |
14
- | LayoutLeftPanel | `LayoutLeftPanel` | 佈局左側面板區塊 |
15
- | LayoutMain | `LayoutMain` | 佈局主要內容區塊 |
16
- | LayoutRightPanel | `LayoutRightPanel` | 佈局右側面板區塊 |
17
- | Separator | `Separator` | 水平或垂直分隔線,用於視覺上區隔內容區塊 |
18
- | Typography | `Typography` | 文字排版元件,支援 h1–h6 / body / caption 等語意層級與顏色變體 |
7
+ | 元件 | 匯入名稱 | 說明 |
8
+ | ---------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | Button | `Button` | 動作觸發按鈕,支援 primary / secondary / ghost / destructive / inverse 變體與多種尺寸 |
10
+ | ButtonGroup | `ButtonGroup` | 將多個 Button 組合為群組,支援水平或垂直排列 |
11
+ | Cropper | `Cropper` | 圖片裁切元件,支援自訂裁切區域與輸出尺寸 |
12
+ | Icon | `Icon` | SVG 圖示元件,搭配 `@mezzanine-ui/icons` 使用,支援顏色與大小控制 |
13
+ | Layout | `Layout` | 頁面整體佈局容器,提供左側面板、主要內容區、右側面板的三欄結構 |
14
+ | LayoutLeftPanel | `LayoutLeftPanel` | 佈局左側面板區塊 |
15
+ | LayoutMain | `LayoutMain` | 佈局主要內容區塊 |
16
+ | LayoutRightPanel | `LayoutRightPanel` | 佈局右側面板區塊 |
17
+ | Separator | `Separator` | 水平或垂直分隔線,用於視覺上區隔內容區塊 |
18
+ | Typography | `Typography` | 文字排版元件,`variant` 為語意排版類型(`TypographySemanticType`),僅支援 `h1`、`h2`、`h3`(無 h4–h6),並提供 body / caption / annotation / button / input / label / text-link 系列共 21 種 variant |
19
19
 
20
20
  ## Navigation(導航)
21
21
 
@@ -110,7 +110,7 @@
110
110
  | SelectTriggerTags | `SelectTriggerTags` | 多選 Select 的標籤顯示區域 | — |
111
111
  | SelectionCard | `SelectionCard` | 可選取的卡片元件,作為 Checkbox/Radio 的卡片式替代 | — |
112
112
  | Slider | `Slider` | 滑桿元件,支援單點與範圍兩種模式 | `useSlider` |
113
- | Toggle | `Toggle` | 切換開關元件,用於表示開/關二元狀態 | `useSwitchControlValue` |
113
+ | Toggle | `Toggle` | 切換開關元件,用於表示開/關二元狀態(原名為 Switch) | `useSwitchControlValue` |
114
114
  | Textarea | `Textarea` | 多行文字輸入框,支援自動調整高度與字數限制 | `useInputControlValue` |
115
115
  | TextField | `TextField` | 文字欄位基底元件,提供通用的邊框/尺寸/狀態樣式 | — |
116
116
  | TimePicker | `TimePicker` | 時間選擇器,點擊觸發 TimePanel 面板選取時間 | `usePickerValue` |
@@ -180,6 +180,7 @@
180
180
  | Scale | `Scale` | 縮放動畫 |
181
181
  | Slide | `Slide` | 滑動動畫,Drawer 使用此元件進行進出場 |
182
182
  | Translate | `Translate` | 位移動畫 |
183
+ | createNotifier | `createNotifier` | 工廠函式,建立通知管理器實例,用於指令式顯示 toast / 通知訊息 |
183
184
 
184
185
  ---
185
186
 
@@ -265,3 +266,15 @@
265
266
  | `useTableSuperContext` | 取得 Table 的上層 Context |
266
267
  | `useTableDataSource` | 管理 Table 的資料來源、排序與篩選 |
267
268
  | `useTableRowSelection` | 管理 Table 的列選取狀態 |
269
+
270
+ ---
271
+
272
+ ## 內部元件(不建議使用)
273
+
274
+ 以下元件僅可透過 sub-path import 存取,為 mezzanine 內部使用。
275
+
276
+ | 元件 | 替代方案 | 說明 |
277
+ | ------------- | -------------------------- | ------------- |
278
+ | ContentHeader | 自行組合 PageHeader 或自訂 | 頁面級別標頭 |
279
+ | ClearActions | 自行組合 Button | 清除/關閉按鈕 |
280
+ | Scrollbar | 瀏覽器原生捲軸或自訂 | 自訂捲軸元件 |
@@ -117,6 +117,23 @@ const Checkbox = forwardRef(function Checkbox(props, ref) {
117
117
  }
118
118
  prevIsCheckedRef.current = isChecked;
119
119
  }, [isChecked, shouldShowEditableInput]);
120
+ const handleChipHostClick = useCallback((e) => {
121
+ var _a;
122
+ if (disabled)
123
+ return;
124
+ if (e.target === e.currentTarget) {
125
+ (_a = inputElementRef.current) === null || _a === void 0 ? void 0 : _a.click();
126
+ }
127
+ }, [disabled]);
128
+ const handleChipHostKeyDown = useCallback((e) => {
129
+ var _a;
130
+ if (disabled)
131
+ return;
132
+ if (e.target === e.currentTarget && (e.key === 'Enter' || e.key === ' ')) {
133
+ e.preventDefault();
134
+ (_a = inputElementRef.current) === null || _a === void 0 ? void 0 : _a.click();
135
+ }
136
+ }, [disabled]);
120
137
  const handleEditableInputMouseDown = useCallback((event) => {
121
138
  var _a, _b;
122
139
  (_b = (_a = defaultEditableInput === null || defaultEditableInput === void 0 ? void 0 : defaultEditableInput.inputProps) === null || _a === void 0 ? void 0 : _a.onMouseDown) === null || _b === void 0 ? void 0 : _b.call(_a, event);
@@ -134,7 +151,7 @@ const Checkbox = forwardRef(function Checkbox(props, ref) {
134
151
  [checkboxClasses.disabled]: disabled,
135
152
  [checkboxClasses.mode(mode)]: mode !== 'default',
136
153
  ...(severity && { [checkboxClasses.severity(severity)]: true }),
137
- }, checkboxClasses.size(size)), children: [jsxs("label", { ref: ref, ...rest, className: checkboxClasses.labelContainer, children: [jsx("div", { className: checkboxClasses.inputContainer, children: jsxs("div", { className: checkboxClasses.inputContent, children: [jsx("input", { ...restInputProps, "aria-checked": isIndeterminate ? 'mixed' : checked, "aria-disabled": disabled, checked: isChecked, className: checkboxClasses.input, disabled: disabled, id: finalInputId, name: resolvedName, onChange: onChange, ref: composedInputRef, type: "checkbox", value: value }), mode === 'chip' && isChecked && (jsx(Icon, { "aria-hidden": "true", className: cx(checkboxClasses.icon, checkboxClasses.chipIcon), color: "brand", icon: CheckedIcon, size: 16 })), mode !== 'chip' && isChecked && (jsx(Icon, { "aria-hidden": "true", className: checkboxClasses.icon, color: "fixed-light", icon: CheckedIcon, size: 9 })), mode !== 'chip' && isIndeterminate && (jsx("span", { "aria-hidden": "true", className: checkboxClasses.indeterminateLine }))] }) }), (label || description) && (jsxs("span", { className: checkboxClasses.textContainer, children: [label && (jsx(Typography, { className: checkboxClasses.label, color: labelColor, variant: "label-primary", children: label })), description && mode !== 'chip' && !shouldShowEditableInput && (jsx(Typography, { className: checkboxClasses.description, color: "text-neutral", variant: "caption", children: description }))] }))] }), shouldShowEditableInput &&
154
+ }, checkboxClasses.size(size)), onClick: mode === 'chip' ? handleChipHostClick : undefined, onKeyDown: mode === 'chip' ? handleChipHostKeyDown : undefined, children: [jsxs("label", { ref: ref, ...rest, className: checkboxClasses.labelContainer, children: [jsx("div", { className: checkboxClasses.inputContainer, children: jsxs("div", { className: checkboxClasses.inputContent, children: [jsx("input", { ...restInputProps, "aria-checked": isIndeterminate ? 'mixed' : checked, "aria-disabled": disabled, checked: isChecked, className: checkboxClasses.input, disabled: disabled, id: finalInputId, name: resolvedName, onChange: onChange, ref: composedInputRef, type: "checkbox", value: value }), mode === 'chip' && isChecked && (jsx(Icon, { "aria-hidden": "true", className: cx(checkboxClasses.icon, checkboxClasses.chipIcon), color: "brand", icon: CheckedIcon, size: 16 })), mode !== 'chip' && isChecked && (jsx(Icon, { "aria-hidden": "true", className: checkboxClasses.icon, color: "fixed-light", icon: CheckedIcon, size: 9 })), mode !== 'chip' && isIndeterminate && (jsx("span", { "aria-hidden": "true", className: checkboxClasses.indeterminateLine }))] }) }), (label || description) && (jsxs("span", { className: checkboxClasses.textContainer, children: [label && (jsx(Typography, { className: checkboxClasses.label, color: labelColor, variant: "label-primary", children: label })), description && mode !== 'chip' && !shouldShowEditableInput && (jsx(Typography, { className: checkboxClasses.description, color: "text-neutral", variant: "caption", children: description }))] }))] }), shouldShowEditableInput &&
138
155
  defaultEditableInput &&
139
156
  mode !== 'chip' &&
140
157
  !indeterminate && (jsx("label", { className: checkboxClasses.editableInputContainer, htmlFor: finalInputId, children: jsx(Input, { ...defaultEditableInput, ...(disabled &&
@@ -221,17 +221,19 @@ function Dropdown(props) {
221
221
  const offsetMiddleware = useMemo(() => {
222
222
  return offset({ mainAxis: 4 });
223
223
  }, []);
224
- // Set z-index for popper to ensure it appears above other elements
224
+ // Set z-index for popper only when explicitly provided via the `zIndex` prop.
225
+ // When not provided, do NOT apply any inline z-index so that elements inside
226
+ // the portal stack purely by DOM order — this ensures Modals opened from
227
+ // a Dropdown always appear on top without a z-index fight.
225
228
  const zIndexMiddleware = useMemo(() => {
226
- const zIndexValue = zIndex !== null && zIndex !== void 0 ? zIndex : 1;
229
+ if (zIndex === undefined || zIndex === null)
230
+ return null;
231
+ const zIndexNum = typeof zIndex === 'number'
232
+ ? zIndex
233
+ : parseInt(zIndex, 10) || zIndex;
227
234
  return {
228
235
  name: 'zIndex',
229
236
  fn: ({ elements }) => {
230
- const zIndexNum = typeof zIndexValue === 'number'
231
- ? zIndexValue
232
- : typeof zIndexValue === 'string'
233
- ? parseInt(zIndexValue, 10) || zIndexValue
234
- : 1;
235
237
  Object.assign(elements.floating.style, {
236
238
  zIndex: zIndexNum,
237
239
  });
@@ -558,7 +560,7 @@ function Dropdown(props) {
558
560
  placement: popoverPlacement,
559
561
  middleware: [
560
562
  offsetMiddleware,
561
- zIndexMiddleware,
563
+ ...(zIndexMiddleware ? [zIndexMiddleware] : []),
562
564
  ...(customWidthMiddleware ? [customWidthMiddleware] : []),
563
565
  ...(sameWidthMiddleware ? [sameWidthMiddleware] : []),
564
566
  ],
@@ -530,7 +530,7 @@ function DropdownItem(props) {
530
530
  onScroll,
531
531
  ]);
532
532
  return (jsxs("ul", { "aria-label": listboxLabel ||
533
- (optionsContent.length === 0 ? 'Dropdown options' : undefined), className: dropdownClasses.list, id: listboxId, ref: listRef, role: "listbox", style: listStyle, tabIndex: -1, children: [hasHeader && (jsx("li", { className: dropdownClasses.listHeader, role: "presentation", ref: headerRef, children: jsx("div", { className: dropdownClasses.listHeaderInner, children: headerContent }) })), maxHeight ? (shouldUseScrollbar ? (jsx(Scrollbar, { className: dropdownClasses.listWrapper, defer: scrollbarDefer, disabled: false, events: scrollbarEvents, maxHeight: listWrapperStyle === null || listWrapperStyle === void 0 ? void 0 : listWrapperStyle.maxHeight, maxWidth: scrollbarMaxWidth, onViewportReady: handleViewportReady, options: scrollbarOptions, children: shouldShowFullStatus ? (jsx(DropdownStatus, { status: status, loadingText: loadingText, emptyText: emptyText, emptyIcon: emptyIcon })) : (jsxs(Fragment, { children: [renderedOptions, shouldShowBottomLoading && (jsx("li", { className: dropdownClasses.loadingMore, "aria-live": "polite", role: "status", children: jsx(DropdownStatus, { status: "loading", loadingText: loadingText }) }))] })) })) : (jsx("div", { ref: listWrapperRef, className: dropdownClasses.listWrapper, style: listWrapperStyle, children: shouldShowFullStatus ? (jsx(DropdownStatus, { status: status, loadingText: loadingText, emptyText: emptyText, emptyIcon: emptyIcon })) : (jsxs(Fragment, { children: [renderedOptions, shouldShowBottomLoading && (jsx("li", { className: dropdownClasses.loadingMore, "aria-live": "polite", role: "status", children: jsx(DropdownStatus, { status: "loading", loadingText: loadingText }) }))] })) }))) : shouldShowFullStatus ? (jsx(DropdownStatus, { status: status, loadingText: loadingText, emptyText: emptyText, emptyIcon: emptyIcon })) : (jsxs(Fragment, { children: [renderedOptions, shouldShowBottomLoading && (jsx("li", { className: dropdownClasses.loadingMore, "aria-live": "polite", role: "status", children: jsx(DropdownStatus, { status: "loading", loadingText: loadingText }) }))] })), hasActions && (jsx("div", { ref: actionRef, children: jsx(DropdownAction, { ...actionConfig }) }))] }));
533
+ (optionsContent.length === 0 ? 'Dropdown options' : undefined), className: dropdownClasses.list, id: listboxId, ref: listRef, role: "listbox", style: listStyle, tabIndex: -1, children: [hasHeader && (jsx("li", { className: dropdownClasses.listHeader, role: "presentation", ref: headerRef, children: jsx("div", { className: dropdownClasses.listHeaderInner, children: headerContent }) })), maxHeight ? (shouldUseScrollbar ? (jsx(Scrollbar, { className: dropdownClasses.listWrapper, defer: scrollbarDefer, disabled: false, events: scrollbarEvents, maxHeight: listWrapperStyle === null || listWrapperStyle === void 0 ? void 0 : listWrapperStyle.maxHeight, maxWidth: scrollbarMaxWidth, onViewportReady: handleViewportReady, options: scrollbarOptions, children: shouldShowFullStatus ? (jsx(DropdownStatus, { status: status, loadingText: loadingText, emptyText: emptyText, emptyIcon: emptyIcon })) : (jsxs(Fragment, { children: [renderedOptions, shouldShowBottomLoading && (jsx("li", { className: dropdownClasses.loadingMore, "aria-live": "polite", role: "status", children: jsx(DropdownStatus, { status: "loading", loadingText: loadingText }) }))] })) })) : (jsx("div", { ref: listWrapperRef, className: dropdownClasses.listWrapper, style: listWrapperStyle, children: shouldShowFullStatus ? (jsx(DropdownStatus, { status: status, loadingText: loadingText, emptyText: emptyText, emptyIcon: emptyIcon })) : (jsxs(Fragment, { children: [renderedOptions, shouldShowBottomLoading && (jsx("li", { className: dropdownClasses.loadingMore, "aria-live": "polite", role: "status", children: jsx(DropdownStatus, { status: "loading", loadingText: loadingText }) }))] })) }))) : shouldShowFullStatus ? (jsx(DropdownStatus, { status: status, loadingText: loadingText, emptyText: emptyText, emptyIcon: emptyIcon })) : (jsxs(Fragment, { children: [renderedOptions, shouldShowBottomLoading && (jsx("li", { className: dropdownClasses.loadingMore, "aria-live": "polite", role: "status", children: jsx(DropdownStatus, { status: "loading", loadingText: loadingText }) }))] })), hasActions && (jsx("div", { ref: actionRef, style: { position: 'relative', zIndex: 0 }, children: jsx(DropdownAction, { ...actionConfig }) }))] }));
534
534
  }
535
535
 
536
536
  export { DropdownItem as default };
@@ -131,7 +131,7 @@ const Navigation = forwardRef((props, ref) => {
131
131
  handleCollapseChange,
132
132
  setActivatedPath: combineSetActivatedPath,
133
133
  optionsAnchorComponent,
134
- }, children: [headerComponent, jsx(NavigationOptionLevelContext.Provider, { value: navigationOptionLevelContextDefaultValues, children: jsxs("div", { ref: contentRef, className: navigationClasses.content, children: [filter && (jsx(Input, { size: "sub", variant: "search", className: cx(navigationClasses.searchInput), value: filterText, onChange: (e) => setFilterText(e.target.value) })), jsx(Scrollbar, { disabled: collapsed, style: { flex: '1 1 0', minHeight: 0 }, children: jsxs("ul", { className: navigationClasses.list, children: [items, collapsed &&
134
+ }, children: [headerComponent, jsx(NavigationOptionLevelContext.Provider, { value: navigationOptionLevelContextDefaultValues, children: jsxs("div", { ref: contentRef, className: navigationClasses.content, children: [filter && (jsx("div", { className: navigationClasses.searchInput, children: jsx(Input, { size: "sub", variant: "search", value: filterText, onChange: (e) => setFilterText(e.target.value) }) })), jsx(Scrollbar, { disabled: collapsed, style: { flex: '1 1 0', minHeight: 0 }, children: jsxs("ul", { className: navigationClasses.list, children: [items, collapsed &&
135
135
  visibleCount !== null &&
136
136
  visibleCount < level1Items.length && (jsx(NavigationOverflowMenu, { items: collapsedMenuItems }))] }) })] }) }), footerComponent] }) }));
137
137
  });
@@ -104,7 +104,7 @@ const NavigationOption = forwardRef((props, ref) => {
104
104
  if (!children)
105
105
  setActivatedPath(currentPath);
106
106
  }
107
- }, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, ref: tooltipChildRef, role: "menuitem", tabIndex: 0, children: [icon && jsx(Icon, { className: navigationOptionClasses.icon, icon: icon }), jsx("span", { className: navigationOptionClasses.titleWrapper, children: jsx(Fade, { ref: titleRef, in: collapsed === false || !icon, children: jsx("span", { className: navigationOptionClasses.title, children: title }) }) }), badge, children && (jsx(Icon, { className: navigationOptionClasses.toggleIcon, icon: GroupToggleIcon }))] })) }), children && !collapsed && (jsx(Collapse, { lazyMount: true, className: cx(navigationOptionClasses.childrenWrapper), in: open, children: jsx(NavigationOptionLevelContext.Provider, { value: {
107
+ }, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, ref: tooltipChildRef, role: "menuitem", tabIndex: 0, children: [icon && jsx(Icon, { className: navigationOptionClasses.icon, icon: icon }), jsx("span", { className: navigationOptionClasses.titleWrapper, children: jsx(Fade, { ref: titleRef, in: collapsed === false || !icon, children: jsx("span", { className: navigationOptionClasses.title, children: collapsed && !icon ? Array.from(title).slice(0, 2).join('') : title }) }) }), badge, children && (jsx(Icon, { className: navigationOptionClasses.toggleIcon, icon: GroupToggleIcon }))] })) }), children && !collapsed && (jsx(Collapse, { lazyMount: true, className: cx(navigationOptionClasses.childrenWrapper), in: open, children: jsx(NavigationOptionLevelContext.Provider, { value: {
108
108
  level: currentLevel,
109
109
  path: currentPath,
110
110
  }, children: jsx("ul", { className: navigationOptionClasses.group, children: items }) }) }))] }));
package/PATTERNS.md ADDED
@@ -0,0 +1,759 @@
1
+ # Mezzanine UI 常用 UI 模式
2
+
3
+ > 此檔案為 [`COMPONENTS.md`](./COMPONENTS.md) 的伴隨文件,提供常見 UI 場景的實作範例。
4
+ >
5
+ > 基於 **v1.0.0**(`@mezzanine-ui/react`)
6
+
7
+ ## 目錄
8
+
9
+ - [佈局模式(Layout Patterns)](#佈局模式layout-patterns)
10
+ - [表單模式(Form Patterns)](#表單模式form-patterns)
11
+ - [表格模式(Table Patterns)](#表格模式table-patterns)
12
+ - [對話框模式(Dialog Patterns)](#對話框模式dialog-patterns)
13
+ - [導航模式(Navigation Patterns)](#導航模式navigation-patterns)
14
+ - [載入狀態(Loading States)](#載入狀態loading-states)
15
+ - [錯誤處理(Error Handling)](#錯誤處理error-handling)
16
+ - [通知提示(Notifications)](#通知提示notifications)
17
+
18
+ ---
19
+
20
+ ## 佈局模式(Layout Patterns)
21
+
22
+ ### 完整頁面佈局 + 右側面板
23
+
24
+ `Layout` 元件是頂層佈局容器。`Navigation`、`Layout.LeftPanel`、`Layout.Main`、`Layout.RightPanel` 均作為 `Layout` 的直接子元件傳入,順序可任意,`Layout` 會自動排序至正確的 DOM 位置。
25
+
26
+ ```tsx
27
+ import { useState } from 'react';
28
+ import { Layout, Navigation, NavigationFooter, NavigationHeader, NavigationOption, NavigationOptionCategory, Button, Table } from '@mezzanine-ui/react';
29
+ import { HomeIcon, SettingIcon } from '@mezzanine-ui/icons';
30
+
31
+ function AppWithRightPanel() {
32
+ const [rightPanelOpen, setRightPanelOpen] = useState(false);
33
+ const [selectedItem, setSelectedItem] = useState<DataItem | null>(null);
34
+
35
+ const handleItemClick = (item: DataItem): void => {
36
+ setSelectedItem(item);
37
+ setRightPanelOpen(true);
38
+ };
39
+
40
+ return (
41
+ <Layout>
42
+ <Navigation>
43
+ <NavigationHeader title="品牌名稱" />
44
+ <NavigationOptionCategory title="主選單">
45
+ <NavigationOption icon={HomeIcon} title="首頁" />
46
+ <NavigationOption icon={SettingIcon} title="設定" />
47
+ </NavigationOptionCategory>
48
+ <NavigationFooter />
49
+ </Navigation>
50
+
51
+ <Layout.Main>
52
+ <Table
53
+ columns={columns}
54
+ dataSource={data}
55
+ onRow={(record) => ({
56
+ onClick: () => handleItemClick(record),
57
+ })}
58
+ />
59
+ </Layout.Main>
60
+
61
+ <Layout.RightPanel defaultWidth={400} open={rightPanelOpen}>
62
+ {selectedItem && (
63
+ <>
64
+ <h2>{selectedItem.name}</h2>
65
+ <p>{selectedItem.description}</p>
66
+ <Button onClick={() => setRightPanelOpen(false)}>關閉</Button>
67
+ </>
68
+ )}
69
+ </Layout.RightPanel>
70
+ </Layout>
71
+ );
72
+ }
73
+ ```
74
+
75
+ ### 雙側面板佈局(左 + 右)
76
+
77
+ ```tsx
78
+ import { useState } from 'react';
79
+ import { Layout, Navigation, NavigationFooter, NavigationHeader, NavigationOption } from '@mezzanine-ui/react';
80
+ import { FileIcon, HomeIcon, UserIcon } from '@mezzanine-ui/icons';
81
+
82
+ function AppWithDualPanels() {
83
+ const [activatedPath, setActivatedPath] = useState(['首頁']);
84
+ const [leftOpen, setLeftOpen] = useState(true);
85
+ const [rightOpen, setRightOpen] = useState(false);
86
+
87
+ return (
88
+ <Layout>
89
+ <Navigation
90
+ activatedPath={activatedPath}
91
+ onOptionClick={(path) => {
92
+ if (path) setActivatedPath(path);
93
+ }}
94
+ >
95
+ <NavigationHeader title="Mezzanine" />
96
+ <NavigationOption icon={HomeIcon} title="首頁" />
97
+ <NavigationOption icon={FileIcon} title="文件" />
98
+ <NavigationOption icon={UserIcon} title="會員管理" />
99
+ <NavigationFooter />
100
+ </Navigation>
101
+
102
+ <Layout.LeftPanel defaultWidth={240} open={leftOpen}>
103
+ <div>
104
+ <h2>左側面板</h2>
105
+ <button onClick={() => setLeftOpen(false)}>關閉</button>
106
+ </div>
107
+ </Layout.LeftPanel>
108
+
109
+ <Layout.Main>
110
+ <div>
111
+ <h1>主要內容</h1>
112
+ {!leftOpen && <button onClick={() => setLeftOpen(true)}>開啟左側</button>}
113
+ {!rightOpen && <button onClick={() => setRightOpen(true)}>開啟右側</button>}
114
+ </div>
115
+ </Layout.Main>
116
+
117
+ <Layout.RightPanel defaultWidth={320} open={rightOpen}>
118
+ <div>
119
+ <h2>右側面板</h2>
120
+ <button onClick={() => setRightOpen(false)}>關閉</button>
121
+ </div>
122
+ </Layout.RightPanel>
123
+ </Layout>
124
+ );
125
+ }
126
+ ```
127
+
128
+ > **Props 說明(LayoutRightPanel / LayoutLeftPanel)**
129
+ >
130
+ > | Prop | 型別 | 說明 |
131
+ > | ---------------- | ---------------------------------- | ----------------------------- |
132
+ > | `children` | `ReactNode` | 面板內容 |
133
+ > | `className` | `string` | 自訂 class |
134
+ > | `defaultWidth` | `number` | 初始寬度(px),最小值 240 |
135
+ > | `onWidthChange` | `(width: number) => void` | 拖曳調整寬度時的回呼 |
136
+ > | `open` | `boolean` | 控制面板顯示 / 隱藏 |
137
+ > | `scrollbarProps` | `Omit<ScrollbarProps, 'children'>` | 傳遞給內部 Scrollbar 的 props |
138
+
139
+ ---
140
+
141
+ ## 表單模式(Form Patterns)
142
+
143
+ ### 基本表單
144
+
145
+ ```tsx
146
+ import { useState } from 'react';
147
+ import { Button, FormField, Input, Select } from '@mezzanine-ui/react';
148
+
149
+ const typeOptions = [
150
+ { id: 'personal', name: '個人' },
151
+ { id: 'business', name: '企業' },
152
+ ];
153
+
154
+ function BasicForm() {
155
+ const [email, setEmail] = useState('');
156
+ const [name, setName] = useState('');
157
+ const [type, setType] = useState<{ id: string; name: string } | null>(null);
158
+
159
+ const handleSubmit = () => {
160
+ // 處理表單送出
161
+ };
162
+
163
+ return (
164
+ <form onSubmit={handleSubmit}>
165
+ <FormField label="姓名" name="name" required>
166
+ <Input onChange={(e) => setName(e.target.value)} placeholder="請輸入姓名" value={name} />
167
+ </FormField>
168
+
169
+ <FormField hintText="我們不會公開您的 Email" label="Email" name="email" required>
170
+ <Input onChange={(e) => setEmail(e.target.value)} placeholder="請輸入 Email" value={email} />
171
+ </FormField>
172
+
173
+ <FormField label="類型" name="type">
174
+ <Select onChange={(value) => setType(value)} options={typeOptions} placeholder="請選擇類型" value={type} />
175
+ </FormField>
176
+
177
+ <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
178
+ <Button onClick={() => {}} variant="base-secondary">
179
+ 取消
180
+ </Button>
181
+ <Button onClick={handleSubmit} variant="base-primary">
182
+ 送出
183
+ </Button>
184
+ </div>
185
+ </form>
186
+ );
187
+ }
188
+ ```
189
+
190
+ ### 表單驗證
191
+
192
+ ```tsx
193
+ import { useState } from 'react';
194
+ import { FormField, Input } from '@mezzanine-ui/react';
195
+ import type { SeverityWithInfo } from '@mezzanine-ui/system/severity';
196
+
197
+ function FormWithValidation() {
198
+ const [email, setEmail] = useState('');
199
+ const [error, setError] = useState('');
200
+
201
+ const validateEmail = (value: string) => {
202
+ if (!value) {
203
+ setError('Email 為必填');
204
+ return false;
205
+ }
206
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
207
+ setError('Email 格式不正確');
208
+ return false;
209
+ }
210
+ setError('');
211
+ return true;
212
+ };
213
+
214
+ const severity: SeverityWithInfo = error ? 'error' : 'info';
215
+
216
+ return (
217
+ <FormField hintText={error || undefined} label="Email" name="email" required severity={severity}>
218
+ <Input
219
+ onChange={(e) => {
220
+ setEmail(e.target.value);
221
+ validateEmail(e.target.value);
222
+ }}
223
+ placeholder="請輸入 Email"
224
+ value={email}
225
+ />
226
+ </FormField>
227
+ );
228
+ }
229
+ ```
230
+
231
+ ### 搜尋表單
232
+
233
+ ```tsx
234
+ import { useState } from 'react';
235
+ import { Button, Input } from '@mezzanine-ui/react';
236
+
237
+ function SearchForm() {
238
+ const [keyword, setKeyword] = useState('');
239
+
240
+ const handleSearch = () => {
241
+ // 執行搜尋
242
+ };
243
+
244
+ return (
245
+ <div style={{ display: 'flex', gap: 8 }}>
246
+ <Input clearable onChange={(e) => setKeyword(e.target.value)} placeholder="搜尋..." value={keyword} variant="search" />
247
+ <Button onClick={handleSearch} variant="base-primary">
248
+ 搜尋
249
+ </Button>
250
+ </div>
251
+ );
252
+ }
253
+ ```
254
+
255
+ ---
256
+
257
+ ## 表格模式(Table Patterns)
258
+
259
+ ### 基本資料表格
260
+
261
+ ```tsx
262
+ import { useState } from 'react';
263
+ import { Button, Modal, Table, Tag } from '@mezzanine-ui/react';
264
+
265
+ function DataTable() {
266
+ const [deleteTarget, setDeleteTarget] = useState<DataItem | null>(null);
267
+
268
+ const handleDeleteConfirm = () => {
269
+ if (deleteTarget) {
270
+ handleDelete(deleteTarget.id);
271
+ setDeleteTarget(null);
272
+ }
273
+ };
274
+
275
+ const columns = [
276
+ {
277
+ dataIndex: 'name',
278
+ title: '名稱',
279
+ },
280
+ {
281
+ dataIndex: 'status',
282
+ render: (status: string) => <Tag label={status === 'active' ? '啟用' : '停用'} type="static" />,
283
+ title: '狀態',
284
+ },
285
+ {
286
+ dataIndex: 'createdAt',
287
+ render: (date: string) => new Date(date).toLocaleDateString(),
288
+ title: '建立時間',
289
+ },
290
+ {
291
+ render: (_: unknown, record: DataItem) => (
292
+ <div style={{ display: 'flex', gap: 8 }}>
293
+ <Button onClick={() => handleEdit(record)} size="minor" variant="base-text-link">
294
+ 編輯
295
+ </Button>
296
+ <Button onClick={() => setDeleteTarget(record)} size="minor" variant="destructive-text-link">
297
+ 刪除
298
+ </Button>
299
+ </div>
300
+ ),
301
+ title: '操作',
302
+ },
303
+ ];
304
+
305
+ const data = [
306
+ { createdAt: '2024-01-01', id: '1', name: '項目 1', status: 'active' },
307
+ { createdAt: '2024-01-02', id: '2', name: '項目 2', status: 'inactive' },
308
+ ];
309
+
310
+ return (
311
+ <>
312
+ <Table columns={columns} dataSource={data} />
313
+ <Modal cancelText="取消" confirmText="刪除" modalType="standard" onCancel={() => setDeleteTarget(null)} onClose={() => setDeleteTarget(null)} onConfirm={handleDeleteConfirm} open={!!deleteTarget} showModalFooter showModalHeader size="narrow" title="確認刪除">
314
+ 確定要刪除「{deleteTarget?.name}」嗎?
315
+ </Modal>
316
+ </>
317
+ );
318
+ }
319
+ ```
320
+
321
+ ### 可選取表格
322
+
323
+ ```tsx
324
+ import { useState } from 'react';
325
+ import { Button, Table } from '@mezzanine-ui/react';
326
+ import type { TableRowSelection } from '@mezzanine-ui/react';
327
+
328
+ function SelectableTable() {
329
+ const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
330
+
331
+ const rowSelection: TableRowSelection = {
332
+ onChange: (keys) => setSelectedKeys(keys as string[]),
333
+ selectedRowKeys: selectedKeys,
334
+ };
335
+
336
+ const handleBatchDelete = () => {
337
+ // 批次刪除選取項目
338
+ };
339
+
340
+ return (
341
+ <div>
342
+ {selectedKeys.length > 0 && (
343
+ <div style={{ marginBottom: 16 }}>
344
+ <span>已選取 {selectedKeys.length} 筆</span>
345
+ <Button onClick={handleBatchDelete} size="sub" variant="destructive-secondary">
346
+ 批次刪除
347
+ </Button>
348
+ </div>
349
+ )}
350
+ <Table columns={columns} dataSource={data} rowSelection={rowSelection} />
351
+ </div>
352
+ );
353
+ }
354
+ ```
355
+
356
+ ### 分頁表格
357
+
358
+ ```tsx
359
+ import { useEffect, useState } from 'react';
360
+ import { Pagination, Table, usePagination } from '@mezzanine-ui/react';
361
+
362
+ function PaginatedTable() {
363
+ const [data, setData] = useState([]);
364
+ const [total, setTotal] = useState(0);
365
+ const [currentPage, setCurrentPage] = useState(1);
366
+
367
+ const pagination = usePagination({
368
+ current: currentPage,
369
+ onChange: (page) => {
370
+ setCurrentPage(page);
371
+ fetchData(page, 10);
372
+ },
373
+ pageSize: 10,
374
+ total,
375
+ });
376
+
377
+ useEffect(() => {
378
+ fetchData(1, 10);
379
+ }, []);
380
+
381
+ return (
382
+ <div>
383
+ <Table columns={columns} dataSource={data} />
384
+ <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
385
+ <Pagination {...pagination} />
386
+ </div>
387
+ </div>
388
+ );
389
+ }
390
+ ```
391
+
392
+ ---
393
+
394
+ ## 對話框模式(Dialog Patterns)
395
+
396
+ ### 確認對話框
397
+
398
+ ```tsx
399
+ import { Modal } from '@mezzanine-ui/react';
400
+
401
+ function ConfirmModal({ onClose, onConfirm, open }) {
402
+ return (
403
+ <Modal cancelText="取消" confirmText="刪除" modalType="standard" onCancel={onClose} onClose={onClose} onConfirm={onConfirm} open={open} showModalFooter showModalHeader size="narrow" title="確認刪除">
404
+ 確定要刪除此項目嗎?此操作無法復原。
405
+ </Modal>
406
+ );
407
+ }
408
+ ```
409
+
410
+ ### 表單對話框
411
+
412
+ ```tsx
413
+ import { useState } from 'react';
414
+ import { FormField, Input, Modal } from '@mezzanine-ui/react';
415
+
416
+ function FormModal({ onClose, onSubmit, open }) {
417
+ const [loading, setLoading] = useState(false);
418
+ const [name, setName] = useState('');
419
+
420
+ const handleSubmit = async () => {
421
+ setLoading(true);
422
+ try {
423
+ await onSubmit({ name });
424
+ onClose();
425
+ } finally {
426
+ setLoading(false);
427
+ }
428
+ };
429
+
430
+ return (
431
+ <Modal cancelText="取消" confirmText="確認" loading={loading} modalType="standard" onCancel={onClose} onClose={onClose} onConfirm={handleSubmit} open={open} showModalFooter showModalHeader title="新增項目">
432
+ <FormField label="名稱" name="name" required>
433
+ <Input onChange={(e) => setName(e.target.value)} placeholder="請輸入名稱" value={name} />
434
+ </FormField>
435
+ </Modal>
436
+ );
437
+ }
438
+ ```
439
+
440
+ ### 抽屜詳情頁
441
+
442
+ ```tsx
443
+ import { Button, Description, DescriptionContent, DescriptionGroup, Drawer, DrawerBody, DrawerFooter, DrawerHeader } from '@mezzanine-ui/react';
444
+
445
+ function DetailDrawer({ data, onClose, open }) {
446
+ return (
447
+ <Drawer onClose={onClose} open={open}>
448
+ <DrawerHeader title="項目詳情" />
449
+ <DrawerBody>
450
+ <DescriptionGroup>
451
+ <Description title="名稱">
452
+ <DescriptionContent>{data?.name}</DescriptionContent>
453
+ </Description>
454
+ <Description title="狀態">
455
+ <DescriptionContent>{data?.status}</DescriptionContent>
456
+ </Description>
457
+ <Description title="建立時間">
458
+ <DescriptionContent>{data?.createdAt}</DescriptionContent>
459
+ </Description>
460
+ </DescriptionGroup>
461
+ </DrawerBody>
462
+ <DrawerFooter>
463
+ <Button onClick={onClose} variant="base-secondary">
464
+ 取消
465
+ </Button>
466
+ <Button onClick={() => handleEdit(data)} variant="base-primary">
467
+ 編輯
468
+ </Button>
469
+ </DrawerFooter>
470
+ </Drawer>
471
+ );
472
+ }
473
+ ```
474
+
475
+ ---
476
+
477
+ ## 導航模式(Navigation Patterns)
478
+
479
+ ### 側邊導航
480
+
481
+ ```tsx
482
+ import { useState } from 'react';
483
+ import { Navigation, NavigationFooter, NavigationHeader, NavigationOption, NavigationOptionCategory, NavigationUserMenu } from '@mezzanine-ui/react';
484
+ import { FileIcon, HomeIcon, SettingIcon } from '@mezzanine-ui/icons';
485
+
486
+ function SideNavigation() {
487
+ const [activeKey, setActiveKey] = useState('home');
488
+
489
+ return (
490
+ <Navigation>
491
+ <NavigationHeader title="品牌名稱" />
492
+
493
+ <NavigationOptionCategory title="主選單">
494
+ <NavigationOption active={activeKey === 'home'} icon={HomeIcon} onTriggerClick={() => setActiveKey('home')} title="首頁" />
495
+ <NavigationOption active={activeKey === 'documents'} icon={FileIcon} onTriggerClick={() => setActiveKey('documents')} title="文件" />
496
+ </NavigationOptionCategory>
497
+
498
+ <NavigationOptionCategory title="設定">
499
+ <NavigationOption active={activeKey === 'settings'} icon={SettingIcon} onTriggerClick={() => setActiveKey('settings')} title="系統設定" />
500
+ </NavigationOptionCategory>
501
+
502
+ <NavigationFooter>
503
+ <NavigationUserMenu imgSrc="/avatar.png" />
504
+ </NavigationFooter>
505
+ </Navigation>
506
+ );
507
+ }
508
+ ```
509
+
510
+ ### 分頁導航(Tab)
511
+
512
+ ```tsx
513
+ import { useState } from 'react';
514
+ import { Tab, TabItem } from '@mezzanine-ui/react';
515
+ import type { Key } from 'react';
516
+
517
+ function TabNavigation() {
518
+ const [activeKey, setActiveKey] = useState<Key>('overview');
519
+
520
+ return (
521
+ <div>
522
+ <Tab activeKey={activeKey} onChange={(key) => setActiveKey(key)}>
523
+ <TabItem key="overview">總覽</TabItem>
524
+ <TabItem key="details">詳細資訊</TabItem>
525
+ <TabItem key="history">歷史紀錄</TabItem>
526
+ </Tab>
527
+
528
+ {activeKey === 'overview' && <OverviewContent />}
529
+ {activeKey === 'details' && <DetailsContent />}
530
+ {activeKey === 'history' && <HistoryContent />}
531
+ </div>
532
+ );
533
+ }
534
+ ```
535
+
536
+ ---
537
+
538
+ ## 載入狀態(Loading States)
539
+
540
+ ### 頁面載入
541
+
542
+ ```tsx
543
+ import { useState } from 'react';
544
+ import { Skeleton, Spin } from '@mezzanine-ui/react';
545
+
546
+ function PageLoading() {
547
+ const [loading, setLoading] = useState(true);
548
+ const [data, setData] = useState(null);
549
+
550
+ if (loading) {
551
+ return (
552
+ <div style={{ padding: 24, textAlign: 'center' }}>
553
+ <Spin description="載入中..." loading />
554
+ </div>
555
+ );
556
+ }
557
+
558
+ return <PageContent data={data} />;
559
+ }
560
+
561
+ // 或使用骨架屏
562
+ function PageWithSkeleton() {
563
+ const [loading, setLoading] = useState(true);
564
+
565
+ if (loading) {
566
+ return (
567
+ <div style={{ padding: 24 }}>
568
+ <Skeleton height={24} width={200} />
569
+ <Skeleton height={16} style={{ marginTop: 16 }} width="100%" />
570
+ <Skeleton height={16} style={{ marginTop: 8 }} width="100%" />
571
+ <Skeleton height={16} style={{ marginTop: 8 }} width="80%" />
572
+ </div>
573
+ );
574
+ }
575
+
576
+ return <PageContent />;
577
+ }
578
+ ```
579
+
580
+ ### 按鈕載入
581
+
582
+ ```tsx
583
+ import { useState } from 'react';
584
+ import { Button } from '@mezzanine-ui/react';
585
+
586
+ function SubmitButton() {
587
+ const [loading, setLoading] = useState(false);
588
+
589
+ const handleClick = async () => {
590
+ setLoading(true);
591
+ try {
592
+ await submitData();
593
+ } finally {
594
+ setLoading(false);
595
+ }
596
+ };
597
+
598
+ return (
599
+ <Button loading={loading} onClick={handleClick} variant="base-primary">
600
+ 送出
601
+ </Button>
602
+ );
603
+ }
604
+ ```
605
+
606
+ ---
607
+
608
+ ## 錯誤處理(Error Handling)
609
+
610
+ ### 表單錯誤
611
+
612
+ ```tsx
613
+ import { FormField, Input, InlineMessageGroup } from '@mezzanine-ui/react';
614
+
615
+ function FormWithErrors({ errors }) {
616
+ const nameError = errors.find((e) => e.field === 'name');
617
+
618
+ return (
619
+ <div>
620
+ {errors.length > 0 && (
621
+ <InlineMessageGroup
622
+ items={errors.map((err) => ({
623
+ content: err.message,
624
+ key: err.field,
625
+ severity: 'error' as const,
626
+ }))}
627
+ />
628
+ )}
629
+
630
+ <FormField hintText={nameError?.message} label="名稱" name="name" required severity={nameError ? 'error' : 'info'}>
631
+ <Input placeholder="請輸入名稱" />
632
+ </FormField>
633
+ </div>
634
+ );
635
+ }
636
+ ```
637
+
638
+ ### 空狀態
639
+
640
+ ```tsx
641
+ import { Button, Empty } from '@mezzanine-ui/react';
642
+
643
+ function EmptyState() {
644
+ return (
645
+ <Empty title="尚無資料" type="initial-data">
646
+ <Button onClick={() => handleCreate()}>建立第一筆資料</Button>
647
+ </Empty>
648
+ );
649
+ }
650
+ ```
651
+
652
+ ### 結果狀態頁
653
+
654
+ ```tsx
655
+ import { ResultState } from '@mezzanine-ui/react';
656
+
657
+ function SuccessResult() {
658
+ return (
659
+ <ResultState
660
+ actions={{
661
+ primaryButton: { children: '繼續新增', onClick: () => reset() },
662
+ secondaryButton: { children: '返回列表', onClick: () => navigate('/list') },
663
+ }}
664
+ description="您的變更已成功儲存"
665
+ title="操作成功"
666
+ type="success"
667
+ />
668
+ );
669
+ }
670
+
671
+ function ErrorResult() {
672
+ return (
673
+ <ResultState
674
+ actions={{
675
+ secondaryButton: { children: '重試', onClick: () => retry() },
676
+ }}
677
+ description="發生錯誤,請稍後再試"
678
+ title="操作失敗"
679
+ type="error"
680
+ />
681
+ );
682
+ }
683
+ ```
684
+
685
+ ---
686
+
687
+ ## 通知提示(Notifications)
688
+
689
+ ### Message 訊息提示
690
+
691
+ ```tsx
692
+ import { Button, Message } from '@mezzanine-ui/react';
693
+
694
+ function NotificationExample() {
695
+ const handleSave = async () => {
696
+ try {
697
+ await saveData();
698
+ Message.success('儲存成功');
699
+ } catch (error) {
700
+ Message.error('儲存失敗,請稍後再試');
701
+ }
702
+ };
703
+
704
+ return (
705
+ <Button onClick={handleSave} variant="base-primary">
706
+ 儲存
707
+ </Button>
708
+ );
709
+ }
710
+ ```
711
+
712
+ ### 通知中心
713
+
714
+ ```tsx
715
+ import { Button, NotificationCenter } from '@mezzanine-ui/react';
716
+
717
+ function NotificationExample() {
718
+ const showNotification = () => {
719
+ NotificationCenter.success({
720
+ description: '您的變更已成功儲存',
721
+ duration: 5000,
722
+ title: '操作成功',
723
+ });
724
+ };
725
+
726
+ const showError = () => {
727
+ NotificationCenter.error({
728
+ description: '發生錯誤,請聯絡系統管理員',
729
+ duration: 0, // 不自動關閉
730
+ title: '操作失敗',
731
+ });
732
+ };
733
+
734
+ return (
735
+ <div>
736
+ <Button onClick={showNotification}>顯示成功通知</Button>
737
+ <Button onClick={showError}>顯示錯誤通知</Button>
738
+ </div>
739
+ );
740
+ }
741
+ ```
742
+
743
+ ### AlertBanner 警示橫幅
744
+
745
+ ```tsx
746
+ import { useState } from 'react';
747
+ import { AlertBanner } from '@mezzanine-ui/react';
748
+
749
+ function PageWithBanner() {
750
+ const [showBanner, setShowBanner] = useState(true);
751
+
752
+ return (
753
+ <div>
754
+ {showBanner && <AlertBanner message="系統將於今晚 22:00 進行維護,預計停機 2 小時。" onClose={() => setShowBanner(false)} severity="warning" />}
755
+ <PageContent />
756
+ </div>
757
+ );
758
+ }
759
+ ```
package/README.md CHANGED
@@ -28,13 +28,12 @@ Create a `main.scss` and import it at your app entry point:
28
28
 
29
29
  :root {
30
30
  @include mzn-system.common-variables('default');
31
- @include mzn-system.colors();
32
- @include mzn-system.palette-variables(light);
31
+ @include mzn-system.colors(light);
33
32
  }
34
33
 
35
34
  /* Optional: dark mode */
36
35
  [data-theme='dark'] {
37
- @include mzn-system.palette-variables(dark);
36
+ @include mzn-system.colors(dark);
38
37
  }
39
38
 
40
39
  /* Optional: compact density */
package/llms.txt CHANGED
@@ -43,8 +43,8 @@ import {
43
43
  NavigationUserMenu,
44
44
  } from '@mezzanine-ui/react/Navigation';
45
45
 
46
- // ContentHeader (page-level header with back button + action slots)
47
- import ContentHeader from '@mezzanine-ui/react/ContentHeader';
46
+ // ContentHeader (deprecated — sub-path import only, not in main barrel export)
47
+ // import ContentHeader from '@mezzanine-ui/react/ContentHeader';
48
48
 
49
49
  // Form layout tokens from core (used with FormField)
50
50
  import {
@@ -76,8 +76,8 @@ Required styles — create a `main.scss` and import it at your app entry point.
76
76
  }
77
77
 
78
78
  /* Optional: light/dark palette theming */
79
- :root { @include mzn-system.palette-variables(light); }
80
- [data-theme='dark'] { @include mzn-system.palette-variables(dark); }
79
+ :root { @include mzn-system.colors(light); }
80
+ [data-theme='dark'] { @include mzn-system.colors(dark); }
81
81
 
82
82
  /* Optional: compact density */
83
83
  [data-density='compact'] { @include mzn-system.common-variables(compact); }
@@ -171,11 +171,25 @@ Message.error('操作失敗,請稍後再試');
171
171
 
172
172
  ## Common Pitfalls for AI Assistants
173
173
 
174
- **Typography `variant` is not h1–h6.** The prop is named `variant` but its type is `TypographySemanticType` — a design-system semantic scale, not HTML heading levels. Valid values are `h1`, `h2`, `h3` (no h4/h5/h6), then `body`, `body-highlight`, `body-mono`, `caption`, `caption-highlight`, `annotation`, `annotation-highlight`, `button`, `input`, `label-primary`, `label-secondary`, and a few others. Never use `h4`, `h5`, or `h6`.
174
+ **Typography `variant` only has h1–h3, not h4–h6.** The prop type is `TypographySemanticType` — a design-system semantic scale, not HTML heading levels. The complete set of valid values is:
175
+
176
+ | Group | Variants |
177
+ |-------|---------|
178
+ | Heading | `h1`, `h2`, `h3` |
179
+ | Body | `body`, `body-highlight`, `body-mono`, `body-mono-highlight` |
180
+ | Text link | `text-link-body`, `text-link-caption` |
181
+ | Caption | `caption`, `caption-highlight` |
182
+ | Annotation | `annotation`, `annotation-highlight` |
183
+ | Button | `button`, `button-highlight` |
184
+ | Input | `input`, `input-mono`, `input-highlight` |
185
+ | Label | `label-primary`, `label-primary-highlight`, `label-secondary` |
186
+
187
+ Never use `h4`, `h5`, or `h6` — they do not exist.
175
188
  ```tsx
176
- <Typography variant="h1">Title</Typography> // ✅
177
- <Typography variant="body">Body text</Typography> // ✅
178
- <Typography variant="h5">Subtitle</Typography> // ❌ h5 does not exist
189
+ <Typography variant="h1">Title</Typography> // ✅
190
+ <Typography variant="body-highlight">Bold</Typography> // ✅
191
+ <Typography variant="text-link-body">Link</Typography> //
192
+ <Typography variant="h5">Subtitle</Typography> // ❌ h5 does not exist
179
193
  ```
180
194
 
181
195
  **Button icons use `icon` + `iconType`, not `prefix`/`suffix` JSX.** Pass an icon definition object (from `@mezzanine-ui/icons`) to the `icon` prop and set position via `iconType`: `"leading"` | `"trailing"` | `"icon-only"`. Do not wrap `<Icon />` in `prefix` or `suffix` props — those props do not exist on Button.
@@ -196,5 +210,6 @@ import { PlusIcon } from '@mezzanine-ui/icons';
196
210
  ## Further Reference
197
211
 
198
212
  - Full component index with descriptions: `packages/react/COMPONENTS.md`
213
+ - Common UI pattern examples (Layout, Form, Table, Dialog, etc.): `packages/react/PATTERNS.md`
199
214
  - Complete prop types and JSDoc: `*.d.ts` files in the published package (or `packages/react/src/<ComponentName>/index.tsx`)
200
215
  - GitHub: https://github.com/Mezzanine-UI/mezzanine
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mezzanine-ui/react",
3
- "version": "1.0.0-rc.7",
3
+ "version": "1.0.0",
4
4
  "description": "React components for mezzanine-ui",
5
5
  "author": "Mezzanine",
6
6
  "repository": {
@@ -32,9 +32,9 @@
32
32
  "@floating-ui/dom": "^1.7.4",
33
33
  "@floating-ui/react-dom": "^2.1.6",
34
34
  "@hello-pangea/dnd": "^18.0.1",
35
- "@mezzanine-ui/core": "1.0.0-rc.7",
36
- "@mezzanine-ui/icons": "1.0.0-rc.7",
37
- "@mezzanine-ui/system": "1.0.0-rc.7",
35
+ "@mezzanine-ui/core": "1.0.0",
36
+ "@mezzanine-ui/icons": "1.0.0",
37
+ "@mezzanine-ui/system": "1.0.0",
38
38
  "@tanstack/react-virtual": "^3.13.13",
39
39
  "@types/react-transition-group": "^4.4.12",
40
40
  "clsx": "^2.1.1",