@meta-1/design 0.0.159

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +412 -0
  2. package/package.json +138 -0
  3. package/src/assets/icons/empty.svg +1 -0
  4. package/src/assets/icons/spin.svg +1 -0
  5. package/src/assets/locales/en-us.ts +74 -0
  6. package/src/assets/locales/zh-cn.ts +74 -0
  7. package/src/assets/locales/zh-tw.ts +74 -0
  8. package/src/assets/style/theme.css +173 -0
  9. package/src/components/icons/Empty.tsx +18 -0
  10. package/src/components/icons/Spin.tsx +16 -0
  11. package/src/components/icons/index.ts +2 -0
  12. package/src/components/ui/alert-dialog.tsx +111 -0
  13. package/src/components/ui/alert.tsx +49 -0
  14. package/src/components/ui/avatar.tsx +32 -0
  15. package/src/components/ui/badge.tsx +36 -0
  16. package/src/components/ui/breadcrumb.tsx +92 -0
  17. package/src/components/ui/button.tsx +52 -0
  18. package/src/components/ui/calendar.tsx +56 -0
  19. package/src/components/ui/card.tsx +56 -0
  20. package/src/components/ui/checkbox.tsx +28 -0
  21. package/src/components/ui/command.tsx +137 -0
  22. package/src/components/ui/dialog.tsx +127 -0
  23. package/src/components/ui/dropdown-menu.tsx +217 -0
  24. package/src/components/ui/form.tsx +138 -0
  25. package/src/components/ui/hover-card.tsx +36 -0
  26. package/src/components/ui/input-otp.tsx +66 -0
  27. package/src/components/ui/input.tsx +21 -0
  28. package/src/components/ui/label.tsx +21 -0
  29. package/src/components/ui/navigation-menu.tsx +142 -0
  30. package/src/components/ui/pagination.tsx +118 -0
  31. package/src/components/ui/popover.tsx +40 -0
  32. package/src/components/ui/progress.tsx +22 -0
  33. package/src/components/ui/radio-group.tsx +31 -0
  34. package/src/components/ui/resizable.tsx +46 -0
  35. package/src/components/ui/scroll-area.tsx +46 -0
  36. package/src/components/ui/select.tsx +158 -0
  37. package/src/components/ui/separator.tsx +26 -0
  38. package/src/components/ui/sheet.tsx +101 -0
  39. package/src/components/ui/skeleton.tsx +7 -0
  40. package/src/components/ui/sonner.tsx +23 -0
  41. package/src/components/ui/switch.tsx +26 -0
  42. package/src/components/ui/table.tsx +73 -0
  43. package/src/components/ui/tabs.tsx +40 -0
  44. package/src/components/ui/textarea.tsx +18 -0
  45. package/src/components/ui/tooltip.tsx +46 -0
  46. package/src/components/uix/action/index.tsx +37 -0
  47. package/src/components/uix/alert/index.tsx +43 -0
  48. package/src/components/uix/alert-dialog/index.tsx +109 -0
  49. package/src/components/uix/avatar/index.tsx +25 -0
  50. package/src/components/uix/breadcrumbs/index.tsx +38 -0
  51. package/src/components/uix/broadcast-channel-context/index.tsx +28 -0
  52. package/src/components/uix/button/index.tsx +29 -0
  53. package/src/components/uix/card/index.tsx +32 -0
  54. package/src/components/uix/checkbox/index.tsx +79 -0
  55. package/src/components/uix/checkbox-group/index.tsx +60 -0
  56. package/src/components/uix/combo-select/index.tsx +364 -0
  57. package/src/components/uix/config-provider/index.tsx +31 -0
  58. package/src/components/uix/data-table/index.tsx +491 -0
  59. package/src/components/uix/data-table/style.css +40 -0
  60. package/src/components/uix/date-picker/index.tsx +88 -0
  61. package/src/components/uix/date-range-picker/index.tsx +71 -0
  62. package/src/components/uix/dialog/index.tsx +70 -0
  63. package/src/components/uix/divider/index.tsx +23 -0
  64. package/src/components/uix/dropdown/index.tsx +117 -0
  65. package/src/components/uix/empty/index.tsx +29 -0
  66. package/src/components/uix/filters/index.tsx +105 -0
  67. package/src/components/uix/form/index.tsx +274 -0
  68. package/src/components/uix/image/index.tsx +13 -0
  69. package/src/components/uix/loading/index.tsx +24 -0
  70. package/src/components/uix/message/index.tsx +21 -0
  71. package/src/components/uix/pagination/index.tsx +180 -0
  72. package/src/components/uix/radio-group/index.tsx +35 -0
  73. package/src/components/uix/result/index.tsx +45 -0
  74. package/src/components/uix/select/index.tsx +93 -0
  75. package/src/components/uix/space/index.tsx +24 -0
  76. package/src/components/uix/spin/index.tsx +12 -0
  77. package/src/components/uix/steps/index.tsx +67 -0
  78. package/src/components/uix/switch/index.tsx +33 -0
  79. package/src/components/uix/tooltip/index.tsx +29 -0
  80. package/src/components/uix/tree/index.tsx +39 -0
  81. package/src/components/uix/tree/style.css +75 -0
  82. package/src/components/uix/tree-select/index.tsx +137 -0
  83. package/src/components/uix/tree-table/action.tsx +24 -0
  84. package/src/components/uix/tree-table/config.ts +2 -0
  85. package/src/components/uix/tree-table/index.tsx +86 -0
  86. package/src/components/uix/tree-table/utils.tsx +63 -0
  87. package/src/components/uix/uploader/index.tsx +237 -0
  88. package/src/components/uix/uploader/type.ts +20 -0
  89. package/src/components/uix/uploader/utils.ts +41 -0
  90. package/src/components/uix/value-formatter/index.tsx +59 -0
  91. package/src/hooks/index.ts +2 -0
  92. package/src/hooks/resize.ts +29 -0
  93. package/src/hooks/use.outside.ts +30 -0
  94. package/src/index.ts +159 -0
  95. package/src/lib/formatters.ts +13 -0
  96. package/src/lib/index.ts +4 -0
  97. package/src/lib/is.ts +6 -0
  98. package/src/lib/react-dom.ts +98 -0
  99. package/src/lib/utils.ts +39 -0
@@ -0,0 +1,70 @@
1
+ import type * as React from "react";
2
+ import type { FC, PropsWithChildren } from "react";
3
+
4
+ import {
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogFooter,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ Dialog as UIDialog,
11
+ } from "@meta-1/design/components/ui/dialog";
12
+ import { Spin } from "@meta-1/design/components/uix/spin";
13
+ import { cn } from "@meta-1/design/lib";
14
+
15
+ export type EventCallback = () => void;
16
+
17
+ export interface DialogProps extends PropsWithChildren {
18
+ visible?: boolean;
19
+ onCancel?: EventCallback;
20
+ onOk?: EventCallback;
21
+ className?: string;
22
+ maskClosable?: boolean;
23
+ closable?: boolean;
24
+ title?: string;
25
+ description?: string;
26
+ footer?: React.ReactNode;
27
+ loading?: boolean;
28
+ }
29
+
30
+ export const Dialog: FC<DialogProps> = (props) => {
31
+ const {
32
+ visible,
33
+ onCancel,
34
+ maskClosable = true,
35
+ className,
36
+ closable = true,
37
+ title,
38
+ description,
39
+ footer,
40
+ loading = false,
41
+ } = props;
42
+
43
+ return (
44
+ <UIDialog open={visible}>
45
+ <DialogContent
46
+ aria-describedby={undefined}
47
+ autoFocus={false}
48
+ className={cn(className)}
49
+ onClick={(e) => e.stopPropagation()}
50
+ onCloseClick={onCancel}
51
+ onOverlayClick={maskClosable ? onCancel : () => {}}
52
+ showClose={closable}
53
+ >
54
+ {title || description ? (
55
+ <DialogHeader>
56
+ {title ? <DialogTitle>{title}</DialogTitle> : null}
57
+ {description ? <DialogDescription>{description}</DialogDescription> : null}
58
+ </DialogHeader>
59
+ ) : null}
60
+ <div className="my-2">{props.children}</div>
61
+ {footer ? <DialogFooter>{footer}</DialogFooter> : null}
62
+ {loading ? (
63
+ <div className={cn("absolute top-0 right-0 bottom-0 left-0 bg-white/50", "flex items-center justify-center")}>
64
+ <Spin />
65
+ </div>
66
+ ) : null}
67
+ </DialogContent>
68
+ </UIDialog>
69
+ );
70
+ };
@@ -0,0 +1,23 @@
1
+ import type { PropsWithChildren } from "react";
2
+
3
+ import { cn } from "@meta-1/design/lib/utils";
4
+
5
+ export interface DividerProps extends PropsWithChildren {
6
+ orientation: "left" | "center" | "right";
7
+ className?: string;
8
+ labelClassName?: string;
9
+ borderClassName?: string;
10
+ }
11
+
12
+ export const Divider = (props: DividerProps) => {
13
+ return (
14
+ <div className={cn("relative my-6", props.className)}>
15
+ <div className="absolute inset-0 flex items-center">
16
+ <span className={cn("w-full border-t", props.borderClassName)} />
17
+ </div>
18
+ <div className="relative flex justify-center text-xs uppercase">
19
+ <span className={cn("bg-background px-2 text-muted-foreground", props.labelClassName)}>{props.children}</span>
20
+ </div>
21
+ </div>
22
+ );
23
+ };
@@ -0,0 +1,117 @@
1
+ import type { FC, MouseEvent, PropsWithChildren, ReactNode } from "react";
2
+
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuCheckboxItem,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuLabel,
9
+ DropdownMenuPortal,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuShortcut,
12
+ DropdownMenuSub,
13
+ DropdownMenuSubContent,
14
+ DropdownMenuSubTrigger,
15
+ DropdownMenuTrigger,
16
+ } from "@meta-1/design/components/ui/dropdown-menu";
17
+ import { cn } from "@meta-1/design/lib";
18
+
19
+ type Align = "start" | "center" | "end";
20
+ type Side = "top" | "bottom" | "left" | "right";
21
+
22
+ export type onDropdownMenuClick = (item: DropdownMenuItemProps, e: MouseEvent) => void;
23
+ export type onCheckedChange = (item: DropdownMenuItemProps, checked: boolean) => void;
24
+
25
+ export interface DropdownMenuItemProps {
26
+ label?: ReactNode;
27
+ id: string;
28
+ type: "label" | "separator" | "item" | "checkbox";
29
+ checked?: boolean;
30
+ shortcut?: string;
31
+ disabled?: boolean;
32
+ children?: DropdownMenuItemProps[];
33
+ onItemClick?: onDropdownMenuClick;
34
+ onCheckedChange?: onCheckedChange;
35
+ hidden?: boolean;
36
+ }
37
+
38
+ export interface DropdownProps extends PropsWithChildren {
39
+ items?: DropdownMenuItemProps[];
40
+ className?: string;
41
+ onItemClick?: onDropdownMenuClick;
42
+ onCheckedChange?: onCheckedChange;
43
+ align?: Align;
44
+ side?: Side;
45
+ asChild?: boolean;
46
+ hideOnEmpty?: boolean;
47
+ modal?: boolean;
48
+ open?: boolean;
49
+ onOpenChange?: (open: boolean) => void;
50
+ }
51
+
52
+ type Callback = {
53
+ onItemClick?: onDropdownMenuClick;
54
+ onCheckedChange?: onCheckedChange;
55
+ };
56
+
57
+ const renderItem = (item: DropdownMenuItemProps, call: Callback) => {
58
+ const clickCall = item.onItemClick || call.onItemClick;
59
+ const checkedCall = item.onCheckedChange || call.onCheckedChange;
60
+ if (item.type === "label") {
61
+ return <DropdownMenuLabel key={item.id}>{item.label}</DropdownMenuLabel>;
62
+ }
63
+ if (item.type === "separator") {
64
+ return <DropdownMenuSeparator key={item.id} />;
65
+ }
66
+ if (item.type === "checkbox") {
67
+ return (
68
+ <DropdownMenuCheckboxItem
69
+ checked={item.checked}
70
+ className="capitalize"
71
+ key={item.id}
72
+ onCheckedChange={(value) => checkedCall?.(item, value)}
73
+ >
74
+ {item.label}
75
+ </DropdownMenuCheckboxItem>
76
+ );
77
+ }
78
+ if (item.children?.length) {
79
+ return (
80
+ <DropdownMenuSub key={item.id}>
81
+ <DropdownMenuSubTrigger>{item.label}</DropdownMenuSubTrigger>
82
+ <DropdownMenuPortal>
83
+ <DropdownMenuSubContent>{item.children.map((child) => renderItem(child, call))}</DropdownMenuSubContent>
84
+ </DropdownMenuPortal>
85
+ </DropdownMenuSub>
86
+ );
87
+ }
88
+ return (
89
+ <DropdownMenuItem disabled={item.disabled} key={item.id} onClick={(e) => clickCall?.(item, e)}>
90
+ {item.label}
91
+ {item.shortcut && <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut>}
92
+ </DropdownMenuItem>
93
+ );
94
+ };
95
+
96
+ export const Dropdown: FC<DropdownProps> = (props) => {
97
+ const {
98
+ items = [],
99
+ onItemClick,
100
+ onCheckedChange,
101
+ align,
102
+ asChild,
103
+ hideOnEmpty = true,
104
+ open,
105
+ modal = false,
106
+ side,
107
+ } = props;
108
+ if (!items?.length && hideOnEmpty) return null;
109
+ return (
110
+ <DropdownMenu modal={modal} onOpenChange={props.onOpenChange} open={open}>
111
+ <DropdownMenuTrigger asChild={asChild}>{props.children}</DropdownMenuTrigger>
112
+ <DropdownMenuContent align={align} className={cn(props.className)} side={side}>
113
+ {items.filter((item) => !item.hidden).map((child) => renderItem(child, { onItemClick, onCheckedChange }))}
114
+ </DropdownMenuContent>
115
+ </DropdownMenu>
116
+ );
117
+ };
@@ -0,0 +1,29 @@
1
+ import { type FC, type ReactNode, useContext } from "react";
2
+ import classNames from "classnames";
3
+ import get from "lodash/get";
4
+
5
+ import { Empty as IconEmpty } from "@meta-1/design/components/icons";
6
+ import { UIXContext } from "@meta-1/design/components/uix/config-provider";
7
+
8
+ export type EmptyProps = {
9
+ text?: ReactNode;
10
+ icon?: ReactNode;
11
+ className?: string;
12
+ textClassName?: string;
13
+ iconClassName?: string;
14
+ };
15
+
16
+ export const Empty: FC<EmptyProps> = (props) => {
17
+ const { className, iconClassName, textClassName } = props;
18
+ const config = useContext(UIXContext);
19
+ const text = props.text || get(config.locale, "Empty.text");
20
+
21
+ return (
22
+ <div className={classNames("flex flex-col items-center justify-center p-4", className)}>
23
+ <div className={iconClassName}>
24
+ {props.icon || <IconEmpty className="text-6xl text-muted-foreground opacity-60" />}
25
+ </div>
26
+ <div className={classNames("text-secondary-foreground/70 text-xs", textClassName)}>{text}</div>
27
+ </div>
28
+ );
29
+ };
@@ -0,0 +1,105 @@
1
+ import { cloneElement, type FC, type PropsWithChildren, type ReactElement, useContext } from "react";
2
+ import get from "lodash/get";
3
+ import pick from "lodash/pick";
4
+ import type { Control } from "react-hook-form";
5
+ import { Controller, type SubmitHandler, useForm } from "react-hook-form";
6
+
7
+ import { Button } from "@meta-1/design/components/uix/button";
8
+ import { UIXContext } from "@meta-1/design/components/uix/config-provider";
9
+
10
+ export interface FilterItemProps {
11
+ field: string;
12
+ render: () => ReactElement;
13
+ label?: string;
14
+ }
15
+
16
+ export interface FiltersProps extends PropsWithChildren {
17
+ items?: FilterItemProps[];
18
+ // biome-ignore lint/suspicious/noExplicitAny: <defaultValues>
19
+ defaultValues?: any;
20
+ // biome-ignore lint/suspicious/noExplicitAny: <query>
21
+ query?: any;
22
+ loading?: boolean;
23
+ load?: (params?: unknown) => Promise<unknown> | unknown;
24
+ searchText?: string;
25
+ resetText?: string;
26
+ }
27
+
28
+ const renderItem = (item: FilterItemProps, form: { control: Control }) => {
29
+ const ele = item.render();
30
+
31
+ return (
32
+ <div className="m-2 my-1" key={item.field}>
33
+ <div className="mb-1 text-muted-foreground text-sm">{item.label}</div>
34
+ <div>
35
+ <Controller
36
+ control={form.control}
37
+ name={item.field}
38
+ render={({ field }) =>
39
+ // biome-ignore lint/suspicious/noExplicitAny: <cloneElement>
40
+ cloneElement<any>(ele, {
41
+ // biome-ignore lint/suspicious/noExplicitAny: <props>
42
+ ...(ele as any).props,
43
+ ...field,
44
+ value: field.value === 0 ? 0 : field.value || "", // a component is changing an uncontrolled input to be controlled
45
+ })
46
+ }
47
+ />
48
+ </div>
49
+ </div>
50
+ );
51
+ };
52
+
53
+ export const Filters: FC<FiltersProps> = (props) => {
54
+ const { items = [], defaultValues = null, loading, load, query } = props;
55
+ const fields = items.map((item) => item.field);
56
+ const { reset, control, handleSubmit } = useForm({
57
+ defaultValues: pick(query, fields),
58
+ });
59
+
60
+ const config = useContext(UIXContext);
61
+ const searchText = props.searchText || get(config.locale, "Filters.searchText");
62
+ const resetText = props.resetText || get(config.locale, "Filters.resetText");
63
+
64
+ // biome-ignore lint/suspicious/noExplicitAny: <data>
65
+ const onSubmit: SubmitHandler<any> = (data) => {
66
+ load?.({
67
+ ...pick(data, fields),
68
+ page: 1,
69
+ });
70
+ };
71
+
72
+ return (
73
+ <div className="relative">
74
+ <form onSubmit={handleSubmit(onSubmit)}>
75
+ <div className="-mx-2 flex flex-wrap items-end justify-start">
76
+ {items.map((item) => renderItem(item, { control }))}
77
+ {items?.length ? (
78
+ <div className="m-2 my-1 flex flex-1 items-center justify-start">
79
+ <Button disabled={loading} type="submit">
80
+ {searchText}
81
+ </Button>
82
+ <Button
83
+ className="ml-2"
84
+ disabled={loading}
85
+ onClick={() => {
86
+ const filteredDefaultValues = pick(defaultValues, fields);
87
+ load?.({
88
+ ...filteredDefaultValues,
89
+ page: 1,
90
+ });
91
+ reset(filteredDefaultValues);
92
+ }}
93
+ type="reset"
94
+ variant="secondary"
95
+ >
96
+ {resetText}
97
+ </Button>
98
+ </div>
99
+ ) : null}
100
+ </div>
101
+ </form>
102
+ {loading ? <div className="dark:!bg-black/5 absolute top-0 right-0 bottom-0 left-0 bg-white/50" /> : null}
103
+ </div>
104
+ );
105
+ };
@@ -0,0 +1,274 @@
1
+ import {
2
+ Children,
3
+ cloneElement,
4
+ type FC,
5
+ type FormHTMLAttributes,
6
+ forwardRef,
7
+ type PropsWithChildren,
8
+ type ReactElement,
9
+ type ReactNode,
10
+ type Ref,
11
+ useCallback,
12
+ useEffect,
13
+ useImperativeHandle,
14
+ useMemo,
15
+ useRef,
16
+ useState,
17
+ } from "react";
18
+ import { zodResolver } from "@hookform/resolvers/zod";
19
+ import classNames from "classnames";
20
+ import isObject from "lodash/isObject";
21
+ import type { Control, FieldValues, Resolver, WatchObserver } from "react-hook-form";
22
+ import {
23
+ type ControllerRenderProps,
24
+ type DefaultValues,
25
+ type SubmitHandler,
26
+ type UseFormReturn,
27
+ useForm as useFormHook,
28
+ } from "react-hook-form";
29
+ import type { ZodType } from "zod";
30
+
31
+ import {
32
+ FormControl,
33
+ FormDescription,
34
+ FormField,
35
+ FormLabel,
36
+ FormMessage,
37
+ Form as UIForm,
38
+ FormItem as UIFormItem,
39
+ } from "@meta-1/design/components/ui/form";
40
+ import { cn } from "@meta-1/design/lib";
41
+
42
+ // 类型安全的 zodResolver 包装器,处理 zod 4.x 版本兼容性
43
+ const safeZodResolver = <T extends FieldValues>(schema: ZodType<T>): Resolver<T> => {
44
+ // @ts-expect-error - zod 4.x 版本兼容性问题,zodResolver 类型基于 zod 3.x
45
+ return zodResolver(schema);
46
+ };
47
+
48
+ export interface RenderProps extends ControllerRenderProps {
49
+ placeholder?: string;
50
+ }
51
+
52
+ export interface FieldItem<T extends FieldValues> extends PropsWithChildren {
53
+ name: string;
54
+ label?: string | ReactNode;
55
+ description?: string;
56
+ control?: Control<T>;
57
+ className?: string;
58
+ }
59
+
60
+ export type FormInstance<T extends FieldValues> = UseFormReturn<T> & {
61
+ _setForm?: (ref: UseFormReturn<T> | null) => void;
62
+ submit: () => void;
63
+ };
64
+
65
+ export type FormProps<T extends FieldValues> = Omit<FormHTMLAttributes<HTMLFormElement>, "onSubmit"> & {
66
+ schema?: ZodType<T>;
67
+ defaultValues?: DefaultValues<T>;
68
+ onSubmit?: SubmitHandler<T>;
69
+ className?: string;
70
+ onValuesChange?: WatchObserver<T>;
71
+ stopPropagation?: boolean;
72
+ form?: FormInstance<T>;
73
+ };
74
+
75
+ // biome-ignore lint/suspicious/noExplicitAny: <props>
76
+ export const FormItem: FC<FieldItem<any>> = (props) => {
77
+ const render = (field: ControllerRenderProps) => {
78
+ if (Children.count(props.children) === 1) {
79
+ const ele = props.children as ReactElement;
80
+ // biome-ignore lint/suspicious/noExplicitAny: <props>
81
+ const onChangeOrigin = (ele as any).props.onChange;
82
+ const onChange = field.onChange;
83
+ // biome-ignore lint/suspicious/noExplicitAny: <e>
84
+ const onChangeWrap = (e: any) => {
85
+ onChange(e);
86
+ onChangeOrigin?.(e);
87
+ };
88
+ // biome-ignore lint/suspicious/noExplicitAny: <cloneElement>
89
+ return cloneElement<any>(ele, {
90
+ // biome-ignore lint/suspicious/noExplicitAny: <props>
91
+ ...(ele as any).props,
92
+ ...field,
93
+ onChange: onChangeWrap,
94
+ value: field.value === 0 ? 0 : field.value || "", // a component is changing an uncontrolled input to be controlled
95
+ });
96
+ }
97
+ return props.children;
98
+ };
99
+
100
+ return (
101
+ <FormField
102
+ control={props.control}
103
+ name={props.name}
104
+ render={(p) => {
105
+ const { field } = p;
106
+ return (
107
+ <UIFormItem className={classNames("flex flex-col", props.className)}>
108
+ {props.label ? <FormLabel>{props.label}</FormLabel> : null}
109
+ <FormControl>{render(field)}</FormControl>
110
+ {props.description ? <FormDescription>{props.description}</FormDescription> : null}
111
+ <FormMessage />
112
+ </UIFormItem>
113
+ );
114
+ }}
115
+ />
116
+ );
117
+ };
118
+
119
+ function FormInner<T extends FieldValues>(props: FormProps<T>, ref: Ref<FormInstance<T> | undefined>) {
120
+ const {
121
+ schema = null,
122
+ defaultValues,
123
+ onSubmit,
124
+ className,
125
+ onValuesChange,
126
+ stopPropagation = true,
127
+ form: formInstance,
128
+ ...rest
129
+ } = props;
130
+
131
+ const form = useFormHook<T>({
132
+ resolver: schema ? safeZodResolver(schema) : undefined,
133
+ defaultValues: defaultValues,
134
+ });
135
+ form.watch(onValuesChange!);
136
+
137
+ // 使用 useRef 来保持稳定的 innerInstance 引用
138
+ const innerInstanceRef = useRef<FormInstance<T> | null>(null);
139
+
140
+ if (!innerInstanceRef.current) {
141
+ // 创建稳定的实例引用,只创建一次
142
+ innerInstanceRef.current = {
143
+ ...form,
144
+ submit: () => {
145
+ if (onSubmit) {
146
+ form.handleSubmit(onSubmit)();
147
+ }
148
+ },
149
+ };
150
+ }
151
+
152
+ // 当 form 或 onSubmit 变化时,更新实例的方法,但保持引用不变
153
+ useEffect(() => {
154
+ if (innerInstanceRef.current) {
155
+ Object.assign(innerInstanceRef.current, form, {
156
+ submit: () => {
157
+ if (onSubmit) {
158
+ form.handleSubmit(onSubmit)();
159
+ }
160
+ },
161
+ });
162
+ }
163
+ }, [form, onSubmit]);
164
+
165
+ useEffect(() => {
166
+ if (formInstance?._setForm && innerInstanceRef.current) {
167
+ formInstance._setForm(innerInstanceRef.current);
168
+ }
169
+ }, [formInstance]);
170
+
171
+ const children = useMemo<ReactNode>(() => {
172
+ return Children.map(props.children, (child) => {
173
+ if (isObject(child) && "type" in (child as ReactElement)) {
174
+ const ele = child as ReactElement;
175
+ if (ele.type === FormItem) {
176
+ // biome-ignore lint/suspicious/noExplicitAny: <cloneElement>
177
+ return cloneElement<any>(ele, {
178
+ // biome-ignore lint/suspicious/noExplicitAny: <props>
179
+ ...(ele as any).props,
180
+ control: form.control,
181
+ });
182
+ }
183
+ }
184
+ return child;
185
+ });
186
+ }, [form.control, props.children]);
187
+
188
+ useImperativeHandle(ref, () => innerInstanceRef.current!, []);
189
+
190
+ return (
191
+ <UIForm {...form}>
192
+ <form
193
+ {...rest}
194
+ className={cn("space-y-4", className)}
195
+ onSubmit={(e) => {
196
+ stopPropagation && e.stopPropagation();
197
+ if (onSubmit) {
198
+ form.handleSubmit(onSubmit)(e);
199
+ }
200
+ }}
201
+ >
202
+ {children}
203
+ </form>
204
+ </UIForm>
205
+ );
206
+ }
207
+
208
+ const useForm = <T extends FieldValues = FieldValues>(): FormInstance<T> => {
209
+ // 使用 useRef 来保持稳定的引用
210
+ const formRef = useRef<UseFormReturn<T> | null>(null);
211
+ const instanceRef = useRef<FormInstance<T> | null>(null);
212
+
213
+ // 稳定的 _setForm 函数引用
214
+ const _setForm = useCallback((form: UseFormReturn<T> | null) => {
215
+ formRef.current = form;
216
+ if (form && instanceRef.current) {
217
+ // 直接更新实例属性,无需强制重新渲染
218
+ Object.assign(instanceRef.current, form, { _setForm });
219
+ }
220
+ }, []);
221
+
222
+ if (!instanceRef.current) {
223
+ // 创建稳定的实例引用
224
+ instanceRef.current = {
225
+ _setForm,
226
+ submit: () => {
227
+ // 动态获取当前的 form 实例
228
+ },
229
+ } as FormInstance<T>;
230
+ }
231
+
232
+ return instanceRef.current;
233
+ };
234
+
235
+ const useWatch = <T extends FieldValues = FieldValues>(name: string, form: FormInstance<T>) => {
236
+ const [value, setValue] = useState<unknown>(undefined);
237
+
238
+ useEffect(() => {
239
+ if (!form?.watch) return;
240
+
241
+ const subscription = form.watch((data) => {
242
+ const fieldValue = (data as Record<string, unknown>)[name];
243
+ setValue(fieldValue);
244
+ });
245
+
246
+ // 初始化时获取一次值
247
+ if (form.getValues) {
248
+ try {
249
+ const currentValue = form.getValues();
250
+ setValue((currentValue as Record<string, unknown>)[name]);
251
+ } catch {
252
+ // ignore error
253
+ }
254
+ }
255
+
256
+ return () => {
257
+ if (subscription && typeof subscription.unsubscribe === "function") {
258
+ subscription.unsubscribe();
259
+ }
260
+ };
261
+ }, [form, name]);
262
+
263
+ return value;
264
+ };
265
+
266
+ export const Form = Object.assign(
267
+ forwardRef(FormInner) as <T extends FieldValues>(
268
+ props: FormProps<T> & { ref?: Ref<UseFormReturn<T> | undefined> },
269
+ ) => ReactElement,
270
+ {
271
+ useForm,
272
+ useWatch,
273
+ },
274
+ );
@@ -0,0 +1,13 @@
1
+ import type { FC, ImgHTMLAttributes } from "react";
2
+
3
+ export type ImageProps = ImgHTMLAttributes<HTMLImageElement>;
4
+
5
+ export const Image: FC<ImageProps> = (props) => {
6
+ const { height, width, className } = props;
7
+
8
+ return (
9
+ <div className={className} style={{ height, width }}>
10
+ <img alt={props.alt} className="h-full w-full" src={props.src} />
11
+ </div>
12
+ );
13
+ };
@@ -0,0 +1,24 @@
1
+ import type { FC, PropsWithChildren } from "react";
2
+
3
+ import { Spin } from "@meta-1/design/components/uix/spin";
4
+ import { cn } from "@meta-1/design/lib";
5
+
6
+ export interface LoadingProps extends PropsWithChildren {
7
+ loading?: boolean;
8
+ className?: string;
9
+ }
10
+
11
+ export const Loading: FC<LoadingProps> = (props) => {
12
+ const { loading } = props;
13
+
14
+ return (
15
+ <div className={cn("relative", props.className)}>
16
+ {props.children}
17
+ {loading ? (
18
+ <div className="dark:!bg-black/5 absolute top-0 right-0 bottom-0 left-0 flex items-center justify-center bg-white/50">
19
+ <Spin />
20
+ </div>
21
+ ) : null}
22
+ </div>
23
+ );
24
+ };
@@ -0,0 +1,21 @@
1
+ import { type ReactNode, useMemo } from "react";
2
+ import { type ExternalToast, toast } from "sonner";
3
+
4
+ export const useMessage = () => {
5
+ return useMemo(() => {
6
+ return {
7
+ success: (message?: ReactNode, options?: ExternalToast) => {
8
+ toast.success(message, options);
9
+ },
10
+ error: (message?: ReactNode, options?: ExternalToast) => {
11
+ toast.error(message, options);
12
+ },
13
+ warning: (message?: ReactNode, options?: ExternalToast) => {
14
+ toast.warning(message, options);
15
+ },
16
+ info: (message?: ReactNode, options?: ExternalToast) => {
17
+ toast.info(message, options);
18
+ },
19
+ };
20
+ }, []);
21
+ };