@teamix-evo/ui 0.1.1

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 (270) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +336 -0
  3. package/_data.json +12 -0
  4. package/manifest.json +1688 -0
  5. package/package.json +90 -0
  6. package/src/components/accordion/accordion.meta.md +87 -0
  7. package/src/components/accordion/accordion.stories.tsx +67 -0
  8. package/src/components/accordion/accordion.tsx +58 -0
  9. package/src/components/affix/affix.meta.md +80 -0
  10. package/src/components/affix/affix.stories.tsx +57 -0
  11. package/src/components/affix/affix.tsx +97 -0
  12. package/src/components/alert/alert.meta.md +101 -0
  13. package/src/components/alert/alert.stories.tsx +93 -0
  14. package/src/components/alert/alert.tsx +132 -0
  15. package/src/components/alert-dialog/alert-dialog.meta.md +107 -0
  16. package/src/components/alert-dialog/alert-dialog.stories.tsx +81 -0
  17. package/src/components/alert-dialog/alert-dialog.tsx +136 -0
  18. package/src/components/anchor/anchor.meta.md +87 -0
  19. package/src/components/anchor/anchor.stories.tsx +74 -0
  20. package/src/components/anchor/anchor.tsx +130 -0
  21. package/src/components/app/app.meta.md +86 -0
  22. package/src/components/app/app.stories.tsx +62 -0
  23. package/src/components/app/app.tsx +58 -0
  24. package/src/components/aspect-ratio/aspect-ratio.meta.md +81 -0
  25. package/src/components/aspect-ratio/aspect-ratio.stories.tsx +59 -0
  26. package/src/components/aspect-ratio/aspect-ratio.tsx +22 -0
  27. package/src/components/auto-complete/auto-complete.meta.md +102 -0
  28. package/src/components/auto-complete/auto-complete.stories.tsx +93 -0
  29. package/src/components/auto-complete/auto-complete.tsx +205 -0
  30. package/src/components/avatar/avatar.meta.md +94 -0
  31. package/src/components/avatar/avatar.stories.tsx +80 -0
  32. package/src/components/avatar/avatar.tsx +126 -0
  33. package/src/components/badge/badge.meta.md +119 -0
  34. package/src/components/badge/badge.stories.tsx +153 -0
  35. package/src/components/badge/badge.tsx +210 -0
  36. package/src/components/breadcrumb/breadcrumb.meta.md +107 -0
  37. package/src/components/breadcrumb/breadcrumb.stories.tsx +84 -0
  38. package/src/components/breadcrumb/breadcrumb.tsx +122 -0
  39. package/src/components/button/button.meta.md +98 -0
  40. package/src/components/button/button.stories.tsx +235 -0
  41. package/src/components/button/button.tsx +160 -0
  42. package/src/components/button-group/button-group.meta.md +92 -0
  43. package/src/components/button-group/button-group.stories.tsx +90 -0
  44. package/src/components/button-group/button-group.tsx +75 -0
  45. package/src/components/calendar/calendar.meta.md +118 -0
  46. package/src/components/calendar/calendar.stories.tsx +68 -0
  47. package/src/components/calendar/calendar.tsx +107 -0
  48. package/src/components/card/card.meta.md +117 -0
  49. package/src/components/card/card.stories.tsx +112 -0
  50. package/src/components/card/card.tsx +222 -0
  51. package/src/components/carousel/carousel.meta.md +117 -0
  52. package/src/components/carousel/carousel.stories.tsx +84 -0
  53. package/src/components/carousel/carousel.tsx +224 -0
  54. package/src/components/cascader/cascader.meta.md +110 -0
  55. package/src/components/cascader/cascader.stories.tsx +108 -0
  56. package/src/components/cascader/cascader.tsx +198 -0
  57. package/src/components/checkbox/checkbox.meta.md +99 -0
  58. package/src/components/checkbox/checkbox.stories.tsx +130 -0
  59. package/src/components/checkbox/checkbox.tsx +125 -0
  60. package/src/components/collapsible/collapsible.meta.md +80 -0
  61. package/src/components/collapsible/collapsible.stories.tsx +35 -0
  62. package/src/components/collapsible/collapsible.tsx +18 -0
  63. package/src/components/color-picker/color-picker.meta.md +84 -0
  64. package/src/components/color-picker/color-picker.stories.tsx +80 -0
  65. package/src/components/color-picker/color-picker.tsx +160 -0
  66. package/src/components/combobox/combobox.meta.md +93 -0
  67. package/src/components/combobox/combobox.stories.tsx +55 -0
  68. package/src/components/combobox/combobox.tsx +130 -0
  69. package/src/components/command/command.meta.md +104 -0
  70. package/src/components/command/command.stories.tsx +59 -0
  71. package/src/components/command/command.tsx +147 -0
  72. package/src/components/context-menu/context-menu.meta.md +90 -0
  73. package/src/components/context-menu/context-menu.stories.tsx +46 -0
  74. package/src/components/context-menu/context-menu.tsx +191 -0
  75. package/src/components/data-table/data-table.meta.md +149 -0
  76. package/src/components/data-table/data-table.stories.tsx +125 -0
  77. package/src/components/data-table/data-table.tsx +185 -0
  78. package/src/components/date-picker/date-picker.meta.md +106 -0
  79. package/src/components/date-picker/date-picker.stories.tsx +58 -0
  80. package/src/components/date-picker/date-picker.tsx +156 -0
  81. package/src/components/descriptions/descriptions.meta.md +78 -0
  82. package/src/components/descriptions/descriptions.stories.tsx +60 -0
  83. package/src/components/descriptions/descriptions.tsx +129 -0
  84. package/src/components/dialog/dialog.meta.md +105 -0
  85. package/src/components/dialog/dialog.stories.tsx +93 -0
  86. package/src/components/dialog/dialog.tsx +128 -0
  87. package/src/components/drawer/drawer.meta.md +96 -0
  88. package/src/components/drawer/drawer.stories.tsx +54 -0
  89. package/src/components/drawer/drawer.tsx +114 -0
  90. package/src/components/dropdown-menu/dropdown-menu.meta.md +103 -0
  91. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +112 -0
  92. package/src/components/dropdown-menu/dropdown-menu.tsx +195 -0
  93. package/src/components/empty/empty.meta.md +81 -0
  94. package/src/components/empty/empty.stories.tsx +46 -0
  95. package/src/components/empty/empty.tsx +47 -0
  96. package/src/components/field/field.meta.md +116 -0
  97. package/src/components/field/field.stories.tsx +117 -0
  98. package/src/components/field/field.tsx +164 -0
  99. package/src/components/flex/flex.meta.md +94 -0
  100. package/src/components/flex/flex.stories.tsx +112 -0
  101. package/src/components/flex/flex.tsx +122 -0
  102. package/src/components/float-button/float-button.meta.md +87 -0
  103. package/src/components/float-button/float-button.stories.tsx +78 -0
  104. package/src/components/float-button/float-button.tsx +143 -0
  105. package/src/components/form/form.meta.md +131 -0
  106. package/src/components/form/form.stories.tsx +122 -0
  107. package/src/components/form/form.tsx +194 -0
  108. package/src/components/grid/grid.meta.md +87 -0
  109. package/src/components/grid/grid.stories.tsx +99 -0
  110. package/src/components/grid/grid.tsx +130 -0
  111. package/src/components/hover-card/hover-card.meta.md +92 -0
  112. package/src/components/hover-card/hover-card.stories.tsx +68 -0
  113. package/src/components/hover-card/hover-card.tsx +29 -0
  114. package/src/components/image/image.meta.md +94 -0
  115. package/src/components/image/image.stories.tsx +55 -0
  116. package/src/components/image/image.tsx +138 -0
  117. package/src/components/input/input.meta.md +109 -0
  118. package/src/components/input/input.stories.tsx +117 -0
  119. package/src/components/input/input.tsx +213 -0
  120. package/src/components/input-group/input-group.meta.md +92 -0
  121. package/src/components/input-group/input-group.stories.tsx +88 -0
  122. package/src/components/input-group/input-group.tsx +107 -0
  123. package/src/components/input-number/input-number.meta.md +91 -0
  124. package/src/components/input-number/input-number.stories.tsx +87 -0
  125. package/src/components/input-number/input-number.tsx +210 -0
  126. package/src/components/input-otp/input-otp.meta.md +105 -0
  127. package/src/components/input-otp/input-otp.stories.tsx +65 -0
  128. package/src/components/input-otp/input-otp.tsx +97 -0
  129. package/src/components/item/item.meta.md +116 -0
  130. package/src/components/item/item.stories.tsx +113 -0
  131. package/src/components/item/item.tsx +171 -0
  132. package/src/components/kbd/kbd.meta.md +85 -0
  133. package/src/components/kbd/kbd.stories.tsx +70 -0
  134. package/src/components/kbd/kbd.tsx +81 -0
  135. package/src/components/label/label.meta.md +91 -0
  136. package/src/components/label/label.stories.tsx +87 -0
  137. package/src/components/label/label.tsx +66 -0
  138. package/src/components/masonry/masonry.meta.md +85 -0
  139. package/src/components/masonry/masonry.stories.tsx +66 -0
  140. package/src/components/masonry/masonry.tsx +59 -0
  141. package/src/components/mentions/mentions.meta.md +89 -0
  142. package/src/components/mentions/mentions.stories.tsx +75 -0
  143. package/src/components/mentions/mentions.tsx +237 -0
  144. package/src/components/menubar/menubar.meta.md +100 -0
  145. package/src/components/menubar/menubar.stories.tsx +81 -0
  146. package/src/components/menubar/menubar.tsx +232 -0
  147. package/src/components/native-select/native-select.meta.md +88 -0
  148. package/src/components/native-select/native-select.stories.tsx +80 -0
  149. package/src/components/native-select/native-select.tsx +54 -0
  150. package/src/components/navigation-menu/navigation-menu.meta.md +108 -0
  151. package/src/components/navigation-menu/navigation-menu.stories.tsx +112 -0
  152. package/src/components/navigation-menu/navigation-menu.tsx +125 -0
  153. package/src/components/notification/notification.meta.md +91 -0
  154. package/src/components/notification/notification.stories.tsx +96 -0
  155. package/src/components/notification/notification.tsx +84 -0
  156. package/src/components/pagination/pagination.meta.md +127 -0
  157. package/src/components/pagination/pagination.stories.tsx +62 -0
  158. package/src/components/pagination/pagination.tsx +285 -0
  159. package/src/components/popconfirm/popconfirm.meta.md +109 -0
  160. package/src/components/popconfirm/popconfirm.stories.tsx +76 -0
  161. package/src/components/popconfirm/popconfirm.tsx +134 -0
  162. package/src/components/popover/popover.meta.md +97 -0
  163. package/src/components/popover/popover.stories.tsx +82 -0
  164. package/src/components/popover/popover.tsx +55 -0
  165. package/src/components/progress/progress.meta.md +86 -0
  166. package/src/components/progress/progress.stories.tsx +75 -0
  167. package/src/components/progress/progress.tsx +195 -0
  168. package/src/components/radio-group/radio-group.meta.md +103 -0
  169. package/src/components/radio-group/radio-group.stories.tsx +77 -0
  170. package/src/components/radio-group/radio-group.tsx +78 -0
  171. package/src/components/rate/rate.meta.md +87 -0
  172. package/src/components/rate/rate.stories.tsx +81 -0
  173. package/src/components/rate/rate.tsx +153 -0
  174. package/src/components/resizable/resizable.meta.md +92 -0
  175. package/src/components/resizable/resizable.stories.tsx +104 -0
  176. package/src/components/resizable/resizable.tsx +56 -0
  177. package/src/components/result/result.meta.md +90 -0
  178. package/src/components/result/result.stories.tsx +71 -0
  179. package/src/components/result/result.tsx +91 -0
  180. package/src/components/scroll-area/scroll-area.meta.md +84 -0
  181. package/src/components/scroll-area/scroll-area.stories.tsx +41 -0
  182. package/src/components/scroll-area/scroll-area.tsx +51 -0
  183. package/src/components/segmented/segmented.meta.md +103 -0
  184. package/src/components/segmented/segmented.stories.tsx +101 -0
  185. package/src/components/segmented/segmented.tsx +138 -0
  186. package/src/components/select/select.meta.md +110 -0
  187. package/src/components/select/select.stories.tsx +100 -0
  188. package/src/components/select/select.tsx +188 -0
  189. package/src/components/separator/separator.meta.md +74 -0
  190. package/src/components/separator/separator.stories.tsx +71 -0
  191. package/src/components/separator/separator.tsx +104 -0
  192. package/src/components/sheet/sheet.meta.md +97 -0
  193. package/src/components/sheet/sheet.stories.tsx +82 -0
  194. package/src/components/sheet/sheet.tsx +139 -0
  195. package/src/components/sidebar/sidebar.meta.md +131 -0
  196. package/src/components/sidebar/sidebar.stories.tsx +82 -0
  197. package/src/components/sidebar/sidebar.tsx +351 -0
  198. package/src/components/skeleton/skeleton.meta.md +95 -0
  199. package/src/components/skeleton/skeleton.stories.tsx +79 -0
  200. package/src/components/skeleton/skeleton.tsx +144 -0
  201. package/src/components/slider/slider.meta.md +94 -0
  202. package/src/components/slider/slider.stories.tsx +69 -0
  203. package/src/components/slider/slider.tsx +86 -0
  204. package/src/components/sonner/sonner.meta.md +96 -0
  205. package/src/components/sonner/sonner.stories.tsx +91 -0
  206. package/src/components/sonner/sonner.tsx +40 -0
  207. package/src/components/space/space.meta.md +94 -0
  208. package/src/components/space/space.stories.tsx +94 -0
  209. package/src/components/space/space.tsx +106 -0
  210. package/src/components/spinner/spinner.meta.md +76 -0
  211. package/src/components/spinner/spinner.stories.tsx +71 -0
  212. package/src/components/spinner/spinner.tsx +64 -0
  213. package/src/components/statistic/statistic.meta.md +99 -0
  214. package/src/components/statistic/statistic.stories.tsx +71 -0
  215. package/src/components/statistic/statistic.tsx +197 -0
  216. package/src/components/steps/steps.meta.md +102 -0
  217. package/src/components/steps/steps.stories.tsx +75 -0
  218. package/src/components/steps/steps.tsx +170 -0
  219. package/src/components/switch/switch.meta.md +92 -0
  220. package/src/components/switch/switch.stories.tsx +75 -0
  221. package/src/components/switch/switch.tsx +101 -0
  222. package/src/components/table/table.meta.md +95 -0
  223. package/src/components/table/table.stories.tsx +75 -0
  224. package/src/components/table/table.tsx +122 -0
  225. package/src/components/tabs/tabs.meta.md +98 -0
  226. package/src/components/tabs/tabs.stories.tsx +70 -0
  227. package/src/components/tabs/tabs.tsx +119 -0
  228. package/src/components/tag/tag.meta.md +94 -0
  229. package/src/components/tag/tag.stories.tsx +77 -0
  230. package/src/components/tag/tag.tsx +185 -0
  231. package/src/components/textarea/textarea.meta.md +83 -0
  232. package/src/components/textarea/textarea.stories.tsx +63 -0
  233. package/src/components/textarea/textarea.tsx +113 -0
  234. package/src/components/time-picker/time-picker.meta.md +83 -0
  235. package/src/components/time-picker/time-picker.stories.tsx +59 -0
  236. package/src/components/time-picker/time-picker.tsx +94 -0
  237. package/src/components/timeline/timeline.meta.md +102 -0
  238. package/src/components/timeline/timeline.stories.tsx +104 -0
  239. package/src/components/timeline/timeline.tsx +147 -0
  240. package/src/components/toggle/toggle.meta.md +88 -0
  241. package/src/components/toggle/toggle.stories.tsx +66 -0
  242. package/src/components/toggle/toggle.tsx +53 -0
  243. package/src/components/toggle-group/toggle-group.meta.md +90 -0
  244. package/src/components/toggle-group/toggle-group.stories.tsx +83 -0
  245. package/src/components/toggle-group/toggle-group.tsx +78 -0
  246. package/src/components/tooltip/tooltip.meta.md +99 -0
  247. package/src/components/tooltip/tooltip.stories.tsx +71 -0
  248. package/src/components/tooltip/tooltip.tsx +93 -0
  249. package/src/components/tour/tour.meta.md +116 -0
  250. package/src/components/tour/tour.stories.tsx +66 -0
  251. package/src/components/tour/tour.tsx +242 -0
  252. package/src/components/transfer/transfer.meta.md +90 -0
  253. package/src/components/transfer/transfer.stories.tsx +68 -0
  254. package/src/components/transfer/transfer.tsx +251 -0
  255. package/src/components/tree/tree.meta.md +111 -0
  256. package/src/components/tree/tree.stories.tsx +109 -0
  257. package/src/components/tree/tree.tsx +367 -0
  258. package/src/components/tree-select/tree-select.meta.md +100 -0
  259. package/src/components/tree-select/tree-select.stories.tsx +80 -0
  260. package/src/components/tree-select/tree-select.tsx +171 -0
  261. package/src/components/typography/typography.meta.md +102 -0
  262. package/src/components/typography/typography.stories.tsx +115 -0
  263. package/src/components/typography/typography.tsx +245 -0
  264. package/src/components/upload/upload.meta.md +111 -0
  265. package/src/components/upload/upload.stories.tsx +75 -0
  266. package/src/components/upload/upload.tsx +265 -0
  267. package/src/components/watermark/watermark.meta.md +95 -0
  268. package/src/components/watermark/watermark.stories.tsx +78 -0
  269. package/src/components/watermark/watermark.tsx +165 -0
  270. package/src/utils/cn.ts +6 -0
@@ -0,0 +1,237 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '@/utils/cn';
4
+ import { Textarea } from '@/components/textarea/textarea';
5
+
6
+ export interface MentionsOption {
7
+ /** 真实插入到文本中的 value(不含前缀)。 */
8
+ value: string;
9
+ /** 候选列表显示文本(不传则用 value)。 */
10
+ label?: React.ReactNode;
11
+ /** 禁用此选项。 */
12
+ disabled?: boolean;
13
+ }
14
+
15
+ export interface MentionsProps
16
+ extends Omit<
17
+ React.TextareaHTMLAttributes<HTMLTextAreaElement>,
18
+ 'onChange' | 'value' | 'defaultValue' | 'onSelect'
19
+ > {
20
+ /** 候选项(antd `options` 并集)。 */
21
+ options: MentionsOption[];
22
+ /** 受控值。 */
23
+ value?: string;
24
+ /** uncontrolled 初值。 */
25
+ defaultValue?: string;
26
+ /** 文本变化回调。 */
27
+ onChange?: (value: string) => void;
28
+ /**
29
+ * 触发符(antd `prefix` 并集) — 通常是 `@` 或 `#`。
30
+ * @default "@"
31
+ */
32
+ prefix?: string;
33
+ /**
34
+ * 选中候选项后插入文本的尾随分隔(对齐 antd `split` 默认空格)。
35
+ * @default " "
36
+ */
37
+ split?: string;
38
+ /**
39
+ * 用户在 prefix 后输入时的搜索回调,用于业务侧异步刷新 `options`。
40
+ */
41
+ onSearch?: (query: string, prefix: string) => void;
42
+ /**
43
+ * 选中候选项时回调。
44
+ */
45
+ onSelect?: (option: MentionsOption, prefix: string) => void;
46
+ /**
47
+ * 文本框尺寸传透传 Textarea(行数)。
48
+ * @default 3
49
+ */
50
+ rows?: number;
51
+ /** 禁用。 */
52
+ disabled?: boolean;
53
+ }
54
+
55
+ interface TriggerCtx {
56
+ /** prefix 在 value 中的索引(`@` 的位置)。 */
57
+ start: number;
58
+ /** prefix 后到光标位置的查询字符串。 */
59
+ query: string;
60
+ }
61
+
62
+ function getTriggerContext(
63
+ value: string,
64
+ caret: number,
65
+ prefix: string,
66
+ ): TriggerCtx | null {
67
+ if (caret <= 0) return null;
68
+ // Walk backwards from caret until we hit a whitespace, newline, or `prefix`.
69
+ for (let i = caret - 1; i >= 0; i--) {
70
+ const ch = value[i];
71
+ if (ch === prefix) {
72
+ // The char immediately before prefix must be start-of-string / whitespace / newline,
73
+ // otherwise we'd trigger on emails like a@b.
74
+ const prev = i === 0 ? '' : value[i - 1];
75
+ if (prev === '' || /\s/.test(prev)) {
76
+ return { start: i, query: value.slice(i + 1, caret) };
77
+ }
78
+ return null;
79
+ }
80
+ if (/\s/.test(ch)) return null;
81
+ }
82
+ return null;
83
+ }
84
+
85
+ const Mentions = React.forwardRef<HTMLTextAreaElement, MentionsProps>(
86
+ (
87
+ {
88
+ options,
89
+ value,
90
+ defaultValue,
91
+ onChange,
92
+ prefix = '@',
93
+ split = ' ',
94
+ onSearch,
95
+ onSelect,
96
+ rows = 3,
97
+ disabled,
98
+ className,
99
+ onKeyDown,
100
+ ...props
101
+ },
102
+ ref,
103
+ ) => {
104
+ const isControlled = value !== undefined;
105
+ const [internal, setInternal] = React.useState<string>(defaultValue ?? '');
106
+ const current = isControlled ? value! : internal;
107
+
108
+ const innerRef = React.useRef<HTMLTextAreaElement | null>(null);
109
+ React.useImperativeHandle(ref, () => innerRef.current as HTMLTextAreaElement);
110
+
111
+ const [ctx, setCtx] = React.useState<TriggerCtx | null>(null);
112
+ const [activeIdx, setActiveIdx] = React.useState(0);
113
+
114
+ const filtered = React.useMemo(() => {
115
+ if (!ctx) return [];
116
+ const q = ctx.query.toLowerCase();
117
+ return options.filter((o) =>
118
+ q === '' ? true : o.value.toLowerCase().includes(q),
119
+ );
120
+ }, [ctx, options]);
121
+
122
+ React.useEffect(() => {
123
+ setActiveIdx(0);
124
+ }, [ctx?.query]);
125
+
126
+ const update = (next: string) => {
127
+ if (!isControlled) setInternal(next);
128
+ onChange?.(next);
129
+ };
130
+
131
+ const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
132
+ const next = e.target.value;
133
+ update(next);
134
+ const caret = e.target.selectionStart ?? next.length;
135
+ const t = getTriggerContext(next, caret, prefix);
136
+ setCtx(t);
137
+ if (t) onSearch?.(t.query, prefix);
138
+ };
139
+
140
+ const insertOption = (opt: MentionsOption) => {
141
+ if (!ctx || opt.disabled) return;
142
+ const el = innerRef.current;
143
+ const before = current.slice(0, ctx.start);
144
+ const after = current.slice(
145
+ (el?.selectionStart ?? ctx.start + 1 + ctx.query.length),
146
+ );
147
+ const inserted = `${prefix}${opt.value}${split}`;
148
+ const next = `${before}${inserted}${after}`;
149
+ update(next);
150
+ onSelect?.(opt, prefix);
151
+ setCtx(null);
152
+ // Restore caret right after the inserted mention.
153
+ window.requestAnimationFrame(() => {
154
+ if (!el) return;
155
+ const pos = (before + inserted).length;
156
+ el.focus();
157
+ el.setSelectionRange(pos, pos);
158
+ });
159
+ };
160
+
161
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
162
+ if (ctx && filtered.length > 0) {
163
+ if (e.key === 'ArrowDown') {
164
+ e.preventDefault();
165
+ setActiveIdx((i) => (i + 1) % filtered.length);
166
+ return;
167
+ }
168
+ if (e.key === 'ArrowUp') {
169
+ e.preventDefault();
170
+ setActiveIdx((i) => (i - 1 + filtered.length) % filtered.length);
171
+ return;
172
+ }
173
+ if (e.key === 'Enter' || e.key === 'Tab') {
174
+ const opt = filtered[activeIdx];
175
+ if (opt && !opt.disabled) {
176
+ e.preventDefault();
177
+ insertOption(opt);
178
+ return;
179
+ }
180
+ }
181
+ if (e.key === 'Escape') {
182
+ e.preventDefault();
183
+ setCtx(null);
184
+ return;
185
+ }
186
+ }
187
+ onKeyDown?.(e);
188
+ };
189
+
190
+ return (
191
+ <div className={cn('relative', className)}>
192
+ <Textarea
193
+ ref={innerRef}
194
+ value={current}
195
+ rows={rows}
196
+ disabled={disabled}
197
+ onChange={handleChange}
198
+ onKeyDown={handleKeyDown}
199
+ onBlur={() => {
200
+ // Delay so click-on-option can fire first.
201
+ window.setTimeout(() => setCtx(null), 120);
202
+ }}
203
+ {...props}
204
+ />
205
+ {ctx && filtered.length > 0 ? (
206
+ <ul
207
+ role="listbox"
208
+ className="absolute left-0 top-full z-50 mt-1 max-h-60 w-56 overflow-auto rounded-md border bg-popover p-1 text-sm text-popover-foreground shadow-md"
209
+ >
210
+ {filtered.map((opt, i) => (
211
+ <li
212
+ key={opt.value}
213
+ role="option"
214
+ aria-selected={i === activeIdx}
215
+ onMouseDown={(e) => {
216
+ e.preventDefault();
217
+ insertOption(opt);
218
+ }}
219
+ onMouseEnter={() => setActiveIdx(i)}
220
+ className={cn(
221
+ 'cursor-pointer rounded-sm px-2 py-1.5',
222
+ i === activeIdx && !opt.disabled && 'bg-accent text-accent-foreground',
223
+ opt.disabled && 'cursor-not-allowed opacity-50',
224
+ )}
225
+ >
226
+ {opt.label ?? opt.value}
227
+ </li>
228
+ ))}
229
+ </ul>
230
+ ) : null}
231
+ </div>
232
+ );
233
+ },
234
+ );
235
+ Mentions.displayName = 'Mentions';
236
+
237
+ export { Mentions };
@@ -0,0 +1,100 @@
1
+ ---
2
+ id: menubar
3
+ name: Menubar
4
+ type: component
5
+ category: navigation
6
+ since: 0.1.0
7
+ package: "@teamix-evo/ui"
8
+ ---
9
+
10
+ # Menubar
11
+
12
+ 应用顶部菜单栏 — Radix Menubar(典型 macOS 风格,文件 / 编辑 / 视图 / 帮助)。
13
+ **与 DropdownMenu 区别**:Menubar 是**横排多个一级菜单**,鼠标 hover 切换;DropdownMenu 是单个独立下拉。
14
+
15
+ ## When to use
16
+
17
+ - 桌面应用 / 编辑器顶部菜单(VS Code / Figma 风格)
18
+ - 多组关联操作的横向汇总
19
+
20
+ ## When NOT to use
21
+
22
+ - 单按钮触发 → `DropdownMenu`
23
+ - 右键 → `ContextMenu`
24
+ - 主导航(分类页面入口)→ `NavigationMenu`
25
+
26
+ ## Props
27
+
28
+ > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。下表是 `Menubar`(Root) 的 props。
29
+
30
+ <!-- auto:props:begin -->
31
+ _(组件无 `<Name>Props` interface — props 详见 [`menubar.tsx`](./menubar.tsx))_
32
+ <!-- auto:props:end -->
33
+
34
+ ## 依赖
35
+
36
+ > 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成,数据源是 [`manifest.json`](../../../manifest.json)。**手工编辑 marker 之间的内容会在下次生成时被覆盖**。
37
+
38
+ <!-- auto:deps:begin -->
39
+ ### 同库依赖
40
+
41
+ > `teamix-evo ui add menubar` 时,以下 entry 会被自动连带安装(无需手动 add)。
42
+
43
+ | Entry | 类型 | 描述 |
44
+ | --- | --- | --- |
45
+ | `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
46
+
47
+ ### npm 依赖
48
+
49
+ > 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
50
+
51
+ ```bash
52
+ pnpm add @radix-ui/react-menubar@^1.1.0 lucide-react@^0.460.0
53
+ ```
54
+ <!-- auto:deps:end -->
55
+
56
+ > 完整子组件:`Menubar` / `MenubarMenu` / `MenubarTrigger` / `MenubarContent` / `MenubarItem` / `MenubarCheckboxItem` / `MenubarRadioItem` / `MenubarLabel` / `MenubarSeparator` / `MenubarShortcut` / `MenubarSub` / `MenubarSubTrigger` / `MenubarSubContent` / `MenubarGroup` / `MenubarRadioGroup` / `MenubarPortal`。
57
+
58
+ ## AI 生成纪律
59
+
60
+ - **Menubar 内必有 MenubarMenu**:每个一级菜单一个 `<MenubarMenu>`
61
+ - **行业惯例顺序**:文件 / 编辑 / 视图 / 工具 / 帮助 — 不要打乱
62
+ - **快捷键用 `<MenubarShortcut>`**:右对齐,与平台风格一致(macOS ⌘ / Windows Ctrl)
63
+ - **避免太深的 Sub 嵌套**:超过 2 级的子菜单影响可用性
64
+
65
+ ## Examples
66
+
67
+ ```tsx
68
+ import {
69
+ Menubar, MenubarMenu, MenubarTrigger, MenubarContent,
70
+ MenubarItem, MenubarSeparator, MenubarShortcut,
71
+ MenubarSub, MenubarSubTrigger, MenubarSubContent,
72
+ } from '@/components/ui/menubar';
73
+
74
+ <Menubar>
75
+ <MenubarMenu>
76
+ <MenubarTrigger>文件</MenubarTrigger>
77
+ <MenubarContent>
78
+ <MenubarItem>新建文件 <MenubarShortcut>⌘N</MenubarShortcut></MenubarItem>
79
+ <MenubarItem>打开 <MenubarShortcut>⌘O</MenubarShortcut></MenubarItem>
80
+ <MenubarSeparator />
81
+ <MenubarSub>
82
+ <MenubarSubTrigger>分享</MenubarSubTrigger>
83
+ <MenubarSubContent>
84
+ <MenubarItem>邮件</MenubarItem>
85
+ <MenubarItem>消息</MenubarItem>
86
+ </MenubarSubContent>
87
+ </MenubarSub>
88
+ <MenubarSeparator />
89
+ <MenubarItem>退出 <MenubarShortcut>⌘Q</MenubarShortcut></MenubarItem>
90
+ </MenubarContent>
91
+ </MenubarMenu>
92
+ <MenubarMenu>
93
+ <MenubarTrigger>编辑</MenubarTrigger>
94
+ <MenubarContent>
95
+ <MenubarItem>撤销 <MenubarShortcut>⌘Z</MenubarShortcut></MenubarItem>
96
+ <MenubarItem>重做 <MenubarShortcut>⇧⌘Z</MenubarShortcut></MenubarItem>
97
+ </MenubarContent>
98
+ </MenubarMenu>
99
+ </Menubar>
100
+ ```
@@ -0,0 +1,81 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import {
3
+ Menubar,
4
+ MenubarMenu,
5
+ MenubarTrigger,
6
+ MenubarContent,
7
+ MenubarItem,
8
+ MenubarSeparator,
9
+ MenubarShortcut,
10
+ MenubarSub,
11
+ MenubarSubTrigger,
12
+ MenubarSubContent,
13
+ } from './menubar';
14
+
15
+ const meta: Meta<typeof Menubar> = {
16
+ title: '导航 · Navigation/Menubar',
17
+ component: Menubar,
18
+ tags: ['autodocs'],
19
+ };
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof Menubar>;
23
+
24
+ export const Default: Story = {
25
+ render: () => (
26
+ <Menubar>
27
+ <MenubarMenu>
28
+ <MenubarTrigger>文件</MenubarTrigger>
29
+ <MenubarContent>
30
+ <MenubarItem>
31
+ 新建文件 <MenubarShortcut>⌘N</MenubarShortcut>
32
+ </MenubarItem>
33
+ <MenubarItem>
34
+ 打开 <MenubarShortcut>⌘O</MenubarShortcut>
35
+ </MenubarItem>
36
+ <MenubarSeparator />
37
+ <MenubarSub>
38
+ <MenubarSubTrigger>分享</MenubarSubTrigger>
39
+ <MenubarSubContent>
40
+ <MenubarItem>邮件</MenubarItem>
41
+ <MenubarItem>消息</MenubarItem>
42
+ <MenubarItem>复制链接</MenubarItem>
43
+ </MenubarSubContent>
44
+ </MenubarSub>
45
+ <MenubarSeparator />
46
+ <MenubarItem>
47
+ 退出 <MenubarShortcut>⌘Q</MenubarShortcut>
48
+ </MenubarItem>
49
+ </MenubarContent>
50
+ </MenubarMenu>
51
+ <MenubarMenu>
52
+ <MenubarTrigger>编辑</MenubarTrigger>
53
+ <MenubarContent>
54
+ <MenubarItem>
55
+ 撤销 <MenubarShortcut>⌘Z</MenubarShortcut>
56
+ </MenubarItem>
57
+ <MenubarItem>
58
+ 重做 <MenubarShortcut>⇧⌘Z</MenubarShortcut>
59
+ </MenubarItem>
60
+ <MenubarSeparator />
61
+ <MenubarItem>
62
+ 剪切 <MenubarShortcut>⌘X</MenubarShortcut>
63
+ </MenubarItem>
64
+ <MenubarItem>
65
+ 复制 <MenubarShortcut>⌘C</MenubarShortcut>
66
+ </MenubarItem>
67
+ <MenubarItem>
68
+ 粘贴 <MenubarShortcut>⌘V</MenubarShortcut>
69
+ </MenubarItem>
70
+ </MenubarContent>
71
+ </MenubarMenu>
72
+ <MenubarMenu>
73
+ <MenubarTrigger>视图</MenubarTrigger>
74
+ <MenubarContent>
75
+ <MenubarItem>侧边栏</MenubarItem>
76
+ <MenubarItem>状态栏</MenubarItem>
77
+ </MenubarContent>
78
+ </MenubarMenu>
79
+ </Menubar>
80
+ ),
81
+ };
@@ -0,0 +1,232 @@
1
+ import * as React from 'react';
2
+ import * as MenubarPrimitive from '@radix-ui/react-menubar';
3
+ import { Check, ChevronRight, Circle } from 'lucide-react';
4
+
5
+ import { cn } from '@/utils/cn';
6
+
7
+ // Explicit `typeof` annotations sidestep TS2742 — without them the inferred
8
+ // types reference deep `.pnpm/@radix-ui/react-context/...` paths which are
9
+ // "not portable" and break declaration emit / consumer typechecks.
10
+ const MenubarMenu: typeof MenubarPrimitive.Menu = MenubarPrimitive.Menu;
11
+ const MenubarGroup: typeof MenubarPrimitive.Group = MenubarPrimitive.Group;
12
+ const MenubarPortal: typeof MenubarPrimitive.Portal = MenubarPrimitive.Portal;
13
+ const MenubarSub: typeof MenubarPrimitive.Sub = MenubarPrimitive.Sub;
14
+ const MenubarRadioGroup: typeof MenubarPrimitive.RadioGroup =
15
+ MenubarPrimitive.RadioGroup;
16
+
17
+ const Menubar = React.forwardRef<
18
+ React.ElementRef<typeof MenubarPrimitive.Root>,
19
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
20
+ >(({ className, ...props }, ref) => (
21
+ <MenubarPrimitive.Root
22
+ ref={ref}
23
+ className={cn(
24
+ 'flex h-9 items-center gap-1 rounded-md border bg-background p-1 shadow-sm',
25
+ className,
26
+ )}
27
+ {...props}
28
+ />
29
+ ));
30
+ Menubar.displayName = MenubarPrimitive.Root.displayName;
31
+
32
+ const MenubarTrigger = React.forwardRef<
33
+ React.ElementRef<typeof MenubarPrimitive.Trigger>,
34
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
35
+ >(({ className, ...props }, ref) => (
36
+ <MenubarPrimitive.Trigger
37
+ ref={ref}
38
+ className={cn(
39
+ 'flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ ));
45
+ MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
46
+
47
+ const MenubarSubTrigger = React.forwardRef<
48
+ React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
49
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
50
+ inset?: boolean;
51
+ }
52
+ >(({ className, inset, children, ...props }, ref) => (
53
+ <MenubarPrimitive.SubTrigger
54
+ ref={ref}
55
+ className={cn(
56
+ 'flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
57
+ inset && 'pl-8',
58
+ className,
59
+ )}
60
+ {...props}
61
+ >
62
+ {children}
63
+ <ChevronRight className="ml-auto size-4" />
64
+ </MenubarPrimitive.SubTrigger>
65
+ ));
66
+ MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
67
+
68
+ const MenubarSubContent = React.forwardRef<
69
+ React.ElementRef<typeof MenubarPrimitive.SubContent>,
70
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
71
+ >(({ className, ...props }, ref) => (
72
+ <MenubarPrimitive.SubContent
73
+ ref={ref}
74
+ className={cn(
75
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
76
+ className,
77
+ )}
78
+ {...props}
79
+ />
80
+ ));
81
+ MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
82
+
83
+ const MenubarContent = React.forwardRef<
84
+ React.ElementRef<typeof MenubarPrimitive.Content>,
85
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
86
+ >(
87
+ (
88
+ { className, align = 'start', alignOffset = -4, sideOffset = 8, ...props },
89
+ ref,
90
+ ) => (
91
+ <MenubarPrimitive.Portal>
92
+ <MenubarPrimitive.Content
93
+ ref={ref}
94
+ align={align}
95
+ alignOffset={alignOffset}
96
+ sideOffset={sideOffset}
97
+ className={cn(
98
+ 'z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
99
+ className,
100
+ )}
101
+ {...props}
102
+ />
103
+ </MenubarPrimitive.Portal>
104
+ ),
105
+ );
106
+ MenubarContent.displayName = MenubarPrimitive.Content.displayName;
107
+
108
+ const MenubarItem = React.forwardRef<
109
+ React.ElementRef<typeof MenubarPrimitive.Item>,
110
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
111
+ inset?: boolean;
112
+ }
113
+ >(({ className, inset, ...props }, ref) => (
114
+ <MenubarPrimitive.Item
115
+ ref={ref}
116
+ className={cn(
117
+ 'relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
118
+ inset && 'pl-8',
119
+ className,
120
+ )}
121
+ {...props}
122
+ />
123
+ ));
124
+ MenubarItem.displayName = MenubarPrimitive.Item.displayName;
125
+
126
+ const MenubarCheckboxItem = React.forwardRef<
127
+ React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
128
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
129
+ >(({ className, children, checked, ...props }, ref) => (
130
+ <MenubarPrimitive.CheckboxItem
131
+ ref={ref}
132
+ className={cn(
133
+ 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
134
+ className,
135
+ )}
136
+ checked={checked}
137
+ {...props}
138
+ >
139
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
140
+ <MenubarPrimitive.ItemIndicator>
141
+ <Check className="size-4" />
142
+ </MenubarPrimitive.ItemIndicator>
143
+ </span>
144
+ {children}
145
+ </MenubarPrimitive.CheckboxItem>
146
+ ));
147
+ MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
148
+
149
+ const MenubarRadioItem = React.forwardRef<
150
+ React.ElementRef<typeof MenubarPrimitive.RadioItem>,
151
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
152
+ >(({ className, children, ...props }, ref) => (
153
+ <MenubarPrimitive.RadioItem
154
+ ref={ref}
155
+ className={cn(
156
+ 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
157
+ className,
158
+ )}
159
+ {...props}
160
+ >
161
+ <span className="absolute left-2 flex size-3.5 items-center justify-center">
162
+ <MenubarPrimitive.ItemIndicator>
163
+ <Circle className="size-2 fill-current" />
164
+ </MenubarPrimitive.ItemIndicator>
165
+ </span>
166
+ {children}
167
+ </MenubarPrimitive.RadioItem>
168
+ ));
169
+ MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
170
+
171
+ const MenubarLabel = React.forwardRef<
172
+ React.ElementRef<typeof MenubarPrimitive.Label>,
173
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
174
+ inset?: boolean;
175
+ }
176
+ >(({ className, inset, ...props }, ref) => (
177
+ <MenubarPrimitive.Label
178
+ ref={ref}
179
+ className={cn(
180
+ 'px-2 py-1.5 text-sm font-semibold',
181
+ inset && 'pl-8',
182
+ className,
183
+ )}
184
+ {...props}
185
+ />
186
+ ));
187
+ MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
188
+
189
+ const MenubarSeparator = React.forwardRef<
190
+ React.ElementRef<typeof MenubarPrimitive.Separator>,
191
+ React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
192
+ >(({ className, ...props }, ref) => (
193
+ <MenubarPrimitive.Separator
194
+ ref={ref}
195
+ className={cn('-mx-1 my-1 h-px bg-muted', className)}
196
+ {...props}
197
+ />
198
+ ));
199
+ MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
200
+
201
+ const MenubarShortcut = ({
202
+ className,
203
+ ...props
204
+ }: React.HTMLAttributes<HTMLSpanElement>) => (
205
+ <span
206
+ className={cn(
207
+ 'ml-auto text-xs tracking-widest text-muted-foreground',
208
+ className,
209
+ )}
210
+ {...props}
211
+ />
212
+ );
213
+ MenubarShortcut.displayName = 'MenubarShortcut';
214
+
215
+ export {
216
+ Menubar,
217
+ MenubarMenu,
218
+ MenubarTrigger,
219
+ MenubarContent,
220
+ MenubarItem,
221
+ MenubarSeparator,
222
+ MenubarLabel,
223
+ MenubarCheckboxItem,
224
+ MenubarRadioGroup,
225
+ MenubarRadioItem,
226
+ MenubarPortal,
227
+ MenubarSubContent,
228
+ MenubarSubTrigger,
229
+ MenubarGroup,
230
+ MenubarSub,
231
+ MenubarShortcut,
232
+ };