@mezzanine-ui/react 1.0.0-rc.8 → 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.
- package/AutoComplete/AutoComplete.d.ts +1 -1
- package/AutoComplete/AutoComplete.js +5 -6
- package/AutoComplete/AutoCompleteInside.d.ts +0 -4
- package/AutoComplete/AutoCompleteInside.js +2 -2
- package/COMPONENTS.md +26 -13
- package/Checkbox/Checkbox.js +18 -1
- package/Dropdown/Dropdown.js +10 -8
- package/Dropdown/DropdownItem.js +1 -1
- package/Navigation/Navigation.js +1 -1
- package/Navigation/NavigationOption.js +1 -1
- package/PATTERNS.md +759 -0
- package/README.md +2 -3
- package/llms.txt +23 -8
- package/package.json +4 -4
|
@@ -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,
|
|
98
|
-
const { addable = false, asyncData = false, className, clearSearchText = true, createSeparators = [',', '+', '\n'], defaultValue, disabled = disabledFromFormControl || false, disabledOptionsFilter = false, emptyText = '沒有符合的項目', error = severity === 'error' || false,
|
|
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,
|
|
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:
|
|
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,
|
|
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:
|
|
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` |
|
|
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` |
|
|
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 | 瀏覽器原生捲軸或自訂 | 自訂捲軸元件 |
|
package/Checkbox/Checkbox.js
CHANGED
|
@@ -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 &&
|
package/Dropdown/Dropdown.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
],
|
package/Dropdown/DropdownItem.js
CHANGED
|
@@ -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 };
|
package/Navigation/Navigation.js
CHANGED
|
@@ -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",
|
|
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.
|
|
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 (
|
|
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.
|
|
80
|
-
[data-theme='dark'] { @include mzn-system.
|
|
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`
|
|
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">
|
|
178
|
-
<Typography variant="
|
|
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
|
|
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
|
|
36
|
-
"@mezzanine-ui/icons": "1.0.0
|
|
37
|
-
"@mezzanine-ui/system": "1.0.0
|
|
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",
|