@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.
- package/LICENSE +21 -0
- package/README.md +336 -0
- package/_data.json +12 -0
- package/manifest.json +1688 -0
- package/package.json +90 -0
- package/src/components/accordion/accordion.meta.md +87 -0
- package/src/components/accordion/accordion.stories.tsx +67 -0
- package/src/components/accordion/accordion.tsx +58 -0
- package/src/components/affix/affix.meta.md +80 -0
- package/src/components/affix/affix.stories.tsx +57 -0
- package/src/components/affix/affix.tsx +97 -0
- package/src/components/alert/alert.meta.md +101 -0
- package/src/components/alert/alert.stories.tsx +93 -0
- package/src/components/alert/alert.tsx +132 -0
- package/src/components/alert-dialog/alert-dialog.meta.md +107 -0
- package/src/components/alert-dialog/alert-dialog.stories.tsx +81 -0
- package/src/components/alert-dialog/alert-dialog.tsx +136 -0
- package/src/components/anchor/anchor.meta.md +87 -0
- package/src/components/anchor/anchor.stories.tsx +74 -0
- package/src/components/anchor/anchor.tsx +130 -0
- package/src/components/app/app.meta.md +86 -0
- package/src/components/app/app.stories.tsx +62 -0
- package/src/components/app/app.tsx +58 -0
- package/src/components/aspect-ratio/aspect-ratio.meta.md +81 -0
- package/src/components/aspect-ratio/aspect-ratio.stories.tsx +59 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +22 -0
- package/src/components/auto-complete/auto-complete.meta.md +102 -0
- package/src/components/auto-complete/auto-complete.stories.tsx +93 -0
- package/src/components/auto-complete/auto-complete.tsx +205 -0
- package/src/components/avatar/avatar.meta.md +94 -0
- package/src/components/avatar/avatar.stories.tsx +80 -0
- package/src/components/avatar/avatar.tsx +126 -0
- package/src/components/badge/badge.meta.md +119 -0
- package/src/components/badge/badge.stories.tsx +153 -0
- package/src/components/badge/badge.tsx +210 -0
- package/src/components/breadcrumb/breadcrumb.meta.md +107 -0
- package/src/components/breadcrumb/breadcrumb.stories.tsx +84 -0
- package/src/components/breadcrumb/breadcrumb.tsx +122 -0
- package/src/components/button/button.meta.md +98 -0
- package/src/components/button/button.stories.tsx +235 -0
- package/src/components/button/button.tsx +160 -0
- package/src/components/button-group/button-group.meta.md +92 -0
- package/src/components/button-group/button-group.stories.tsx +90 -0
- package/src/components/button-group/button-group.tsx +75 -0
- package/src/components/calendar/calendar.meta.md +118 -0
- package/src/components/calendar/calendar.stories.tsx +68 -0
- package/src/components/calendar/calendar.tsx +107 -0
- package/src/components/card/card.meta.md +117 -0
- package/src/components/card/card.stories.tsx +112 -0
- package/src/components/card/card.tsx +222 -0
- package/src/components/carousel/carousel.meta.md +117 -0
- package/src/components/carousel/carousel.stories.tsx +84 -0
- package/src/components/carousel/carousel.tsx +224 -0
- package/src/components/cascader/cascader.meta.md +110 -0
- package/src/components/cascader/cascader.stories.tsx +108 -0
- package/src/components/cascader/cascader.tsx +198 -0
- package/src/components/checkbox/checkbox.meta.md +99 -0
- package/src/components/checkbox/checkbox.stories.tsx +130 -0
- package/src/components/checkbox/checkbox.tsx +125 -0
- package/src/components/collapsible/collapsible.meta.md +80 -0
- package/src/components/collapsible/collapsible.stories.tsx +35 -0
- package/src/components/collapsible/collapsible.tsx +18 -0
- package/src/components/color-picker/color-picker.meta.md +84 -0
- package/src/components/color-picker/color-picker.stories.tsx +80 -0
- package/src/components/color-picker/color-picker.tsx +160 -0
- package/src/components/combobox/combobox.meta.md +93 -0
- package/src/components/combobox/combobox.stories.tsx +55 -0
- package/src/components/combobox/combobox.tsx +130 -0
- package/src/components/command/command.meta.md +104 -0
- package/src/components/command/command.stories.tsx +59 -0
- package/src/components/command/command.tsx +147 -0
- package/src/components/context-menu/context-menu.meta.md +90 -0
- package/src/components/context-menu/context-menu.stories.tsx +46 -0
- package/src/components/context-menu/context-menu.tsx +191 -0
- package/src/components/data-table/data-table.meta.md +149 -0
- package/src/components/data-table/data-table.stories.tsx +125 -0
- package/src/components/data-table/data-table.tsx +185 -0
- package/src/components/date-picker/date-picker.meta.md +106 -0
- package/src/components/date-picker/date-picker.stories.tsx +58 -0
- package/src/components/date-picker/date-picker.tsx +156 -0
- package/src/components/descriptions/descriptions.meta.md +78 -0
- package/src/components/descriptions/descriptions.stories.tsx +60 -0
- package/src/components/descriptions/descriptions.tsx +129 -0
- package/src/components/dialog/dialog.meta.md +105 -0
- package/src/components/dialog/dialog.stories.tsx +93 -0
- package/src/components/dialog/dialog.tsx +128 -0
- package/src/components/drawer/drawer.meta.md +96 -0
- package/src/components/drawer/drawer.stories.tsx +54 -0
- package/src/components/drawer/drawer.tsx +114 -0
- package/src/components/dropdown-menu/dropdown-menu.meta.md +103 -0
- package/src/components/dropdown-menu/dropdown-menu.stories.tsx +112 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +195 -0
- package/src/components/empty/empty.meta.md +81 -0
- package/src/components/empty/empty.stories.tsx +46 -0
- package/src/components/empty/empty.tsx +47 -0
- package/src/components/field/field.meta.md +116 -0
- package/src/components/field/field.stories.tsx +117 -0
- package/src/components/field/field.tsx +164 -0
- package/src/components/flex/flex.meta.md +94 -0
- package/src/components/flex/flex.stories.tsx +112 -0
- package/src/components/flex/flex.tsx +122 -0
- package/src/components/float-button/float-button.meta.md +87 -0
- package/src/components/float-button/float-button.stories.tsx +78 -0
- package/src/components/float-button/float-button.tsx +143 -0
- package/src/components/form/form.meta.md +131 -0
- package/src/components/form/form.stories.tsx +122 -0
- package/src/components/form/form.tsx +194 -0
- package/src/components/grid/grid.meta.md +87 -0
- package/src/components/grid/grid.stories.tsx +99 -0
- package/src/components/grid/grid.tsx +130 -0
- package/src/components/hover-card/hover-card.meta.md +92 -0
- package/src/components/hover-card/hover-card.stories.tsx +68 -0
- package/src/components/hover-card/hover-card.tsx +29 -0
- package/src/components/image/image.meta.md +94 -0
- package/src/components/image/image.stories.tsx +55 -0
- package/src/components/image/image.tsx +138 -0
- package/src/components/input/input.meta.md +109 -0
- package/src/components/input/input.stories.tsx +117 -0
- package/src/components/input/input.tsx +213 -0
- package/src/components/input-group/input-group.meta.md +92 -0
- package/src/components/input-group/input-group.stories.tsx +88 -0
- package/src/components/input-group/input-group.tsx +107 -0
- package/src/components/input-number/input-number.meta.md +91 -0
- package/src/components/input-number/input-number.stories.tsx +87 -0
- package/src/components/input-number/input-number.tsx +210 -0
- package/src/components/input-otp/input-otp.meta.md +105 -0
- package/src/components/input-otp/input-otp.stories.tsx +65 -0
- package/src/components/input-otp/input-otp.tsx +97 -0
- package/src/components/item/item.meta.md +116 -0
- package/src/components/item/item.stories.tsx +113 -0
- package/src/components/item/item.tsx +171 -0
- package/src/components/kbd/kbd.meta.md +85 -0
- package/src/components/kbd/kbd.stories.tsx +70 -0
- package/src/components/kbd/kbd.tsx +81 -0
- package/src/components/label/label.meta.md +91 -0
- package/src/components/label/label.stories.tsx +87 -0
- package/src/components/label/label.tsx +66 -0
- package/src/components/masonry/masonry.meta.md +85 -0
- package/src/components/masonry/masonry.stories.tsx +66 -0
- package/src/components/masonry/masonry.tsx +59 -0
- package/src/components/mentions/mentions.meta.md +89 -0
- package/src/components/mentions/mentions.stories.tsx +75 -0
- package/src/components/mentions/mentions.tsx +237 -0
- package/src/components/menubar/menubar.meta.md +100 -0
- package/src/components/menubar/menubar.stories.tsx +81 -0
- package/src/components/menubar/menubar.tsx +232 -0
- package/src/components/native-select/native-select.meta.md +88 -0
- package/src/components/native-select/native-select.stories.tsx +80 -0
- package/src/components/native-select/native-select.tsx +54 -0
- package/src/components/navigation-menu/navigation-menu.meta.md +108 -0
- package/src/components/navigation-menu/navigation-menu.stories.tsx +112 -0
- package/src/components/navigation-menu/navigation-menu.tsx +125 -0
- package/src/components/notification/notification.meta.md +91 -0
- package/src/components/notification/notification.stories.tsx +96 -0
- package/src/components/notification/notification.tsx +84 -0
- package/src/components/pagination/pagination.meta.md +127 -0
- package/src/components/pagination/pagination.stories.tsx +62 -0
- package/src/components/pagination/pagination.tsx +285 -0
- package/src/components/popconfirm/popconfirm.meta.md +109 -0
- package/src/components/popconfirm/popconfirm.stories.tsx +76 -0
- package/src/components/popconfirm/popconfirm.tsx +134 -0
- package/src/components/popover/popover.meta.md +97 -0
- package/src/components/popover/popover.stories.tsx +82 -0
- package/src/components/popover/popover.tsx +55 -0
- package/src/components/progress/progress.meta.md +86 -0
- package/src/components/progress/progress.stories.tsx +75 -0
- package/src/components/progress/progress.tsx +195 -0
- package/src/components/radio-group/radio-group.meta.md +103 -0
- package/src/components/radio-group/radio-group.stories.tsx +77 -0
- package/src/components/radio-group/radio-group.tsx +78 -0
- package/src/components/rate/rate.meta.md +87 -0
- package/src/components/rate/rate.stories.tsx +81 -0
- package/src/components/rate/rate.tsx +153 -0
- package/src/components/resizable/resizable.meta.md +92 -0
- package/src/components/resizable/resizable.stories.tsx +104 -0
- package/src/components/resizable/resizable.tsx +56 -0
- package/src/components/result/result.meta.md +90 -0
- package/src/components/result/result.stories.tsx +71 -0
- package/src/components/result/result.tsx +91 -0
- package/src/components/scroll-area/scroll-area.meta.md +84 -0
- package/src/components/scroll-area/scroll-area.stories.tsx +41 -0
- package/src/components/scroll-area/scroll-area.tsx +51 -0
- package/src/components/segmented/segmented.meta.md +103 -0
- package/src/components/segmented/segmented.stories.tsx +101 -0
- package/src/components/segmented/segmented.tsx +138 -0
- package/src/components/select/select.meta.md +110 -0
- package/src/components/select/select.stories.tsx +100 -0
- package/src/components/select/select.tsx +188 -0
- package/src/components/separator/separator.meta.md +74 -0
- package/src/components/separator/separator.stories.tsx +71 -0
- package/src/components/separator/separator.tsx +104 -0
- package/src/components/sheet/sheet.meta.md +97 -0
- package/src/components/sheet/sheet.stories.tsx +82 -0
- package/src/components/sheet/sheet.tsx +139 -0
- package/src/components/sidebar/sidebar.meta.md +131 -0
- package/src/components/sidebar/sidebar.stories.tsx +82 -0
- package/src/components/sidebar/sidebar.tsx +351 -0
- package/src/components/skeleton/skeleton.meta.md +95 -0
- package/src/components/skeleton/skeleton.stories.tsx +79 -0
- package/src/components/skeleton/skeleton.tsx +144 -0
- package/src/components/slider/slider.meta.md +94 -0
- package/src/components/slider/slider.stories.tsx +69 -0
- package/src/components/slider/slider.tsx +86 -0
- package/src/components/sonner/sonner.meta.md +96 -0
- package/src/components/sonner/sonner.stories.tsx +91 -0
- package/src/components/sonner/sonner.tsx +40 -0
- package/src/components/space/space.meta.md +94 -0
- package/src/components/space/space.stories.tsx +94 -0
- package/src/components/space/space.tsx +106 -0
- package/src/components/spinner/spinner.meta.md +76 -0
- package/src/components/spinner/spinner.stories.tsx +71 -0
- package/src/components/spinner/spinner.tsx +64 -0
- package/src/components/statistic/statistic.meta.md +99 -0
- package/src/components/statistic/statistic.stories.tsx +71 -0
- package/src/components/statistic/statistic.tsx +197 -0
- package/src/components/steps/steps.meta.md +102 -0
- package/src/components/steps/steps.stories.tsx +75 -0
- package/src/components/steps/steps.tsx +170 -0
- package/src/components/switch/switch.meta.md +92 -0
- package/src/components/switch/switch.stories.tsx +75 -0
- package/src/components/switch/switch.tsx +101 -0
- package/src/components/table/table.meta.md +95 -0
- package/src/components/table/table.stories.tsx +75 -0
- package/src/components/table/table.tsx +122 -0
- package/src/components/tabs/tabs.meta.md +98 -0
- package/src/components/tabs/tabs.stories.tsx +70 -0
- package/src/components/tabs/tabs.tsx +119 -0
- package/src/components/tag/tag.meta.md +94 -0
- package/src/components/tag/tag.stories.tsx +77 -0
- package/src/components/tag/tag.tsx +185 -0
- package/src/components/textarea/textarea.meta.md +83 -0
- package/src/components/textarea/textarea.stories.tsx +63 -0
- package/src/components/textarea/textarea.tsx +113 -0
- package/src/components/time-picker/time-picker.meta.md +83 -0
- package/src/components/time-picker/time-picker.stories.tsx +59 -0
- package/src/components/time-picker/time-picker.tsx +94 -0
- package/src/components/timeline/timeline.meta.md +102 -0
- package/src/components/timeline/timeline.stories.tsx +104 -0
- package/src/components/timeline/timeline.tsx +147 -0
- package/src/components/toggle/toggle.meta.md +88 -0
- package/src/components/toggle/toggle.stories.tsx +66 -0
- package/src/components/toggle/toggle.tsx +53 -0
- package/src/components/toggle-group/toggle-group.meta.md +90 -0
- package/src/components/toggle-group/toggle-group.stories.tsx +83 -0
- package/src/components/toggle-group/toggle-group.tsx +78 -0
- package/src/components/tooltip/tooltip.meta.md +99 -0
- package/src/components/tooltip/tooltip.stories.tsx +71 -0
- package/src/components/tooltip/tooltip.tsx +93 -0
- package/src/components/tour/tour.meta.md +116 -0
- package/src/components/tour/tour.stories.tsx +66 -0
- package/src/components/tour/tour.tsx +242 -0
- package/src/components/transfer/transfer.meta.md +90 -0
- package/src/components/transfer/transfer.stories.tsx +68 -0
- package/src/components/transfer/transfer.tsx +251 -0
- package/src/components/tree/tree.meta.md +111 -0
- package/src/components/tree/tree.stories.tsx +109 -0
- package/src/components/tree/tree.tsx +367 -0
- package/src/components/tree-select/tree-select.meta.md +100 -0
- package/src/components/tree-select/tree-select.stories.tsx +80 -0
- package/src/components/tree-select/tree-select.tsx +171 -0
- package/src/components/typography/typography.meta.md +102 -0
- package/src/components/typography/typography.stories.tsx +115 -0
- package/src/components/typography/typography.tsx +245 -0
- package/src/components/upload/upload.meta.md +111 -0
- package/src/components/upload/upload.stories.tsx +75 -0
- package/src/components/upload/upload.tsx +265 -0
- package/src/components/watermark/watermark.meta.md +95 -0
- package/src/components/watermark/watermark.stories.tsx +78 -0
- package/src/components/watermark/watermark.tsx +165 -0
- package/src/utils/cn.ts +6 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Anchor } from './anchor';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Anchor> = {
|
|
5
|
+
title: '导航 · Navigation/Anchor',
|
|
6
|
+
component: Anchor,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
'锚点导航 — 长文档 / 详情页侧边目录,点击滚动到 id 节点,滚动时 IntersectionObserver 自动高亮。等价 antd `Anchor`。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof Anchor>;
|
|
20
|
+
|
|
21
|
+
const items = [
|
|
22
|
+
{ key: 'intro', title: '介绍' },
|
|
23
|
+
{
|
|
24
|
+
key: 'install',
|
|
25
|
+
title: '安装',
|
|
26
|
+
children: [
|
|
27
|
+
{ key: 'npm', title: 'npm' },
|
|
28
|
+
{ key: 'pnpm', title: 'pnpm' },
|
|
29
|
+
{ key: 'yarn', title: 'yarn' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{ key: 'usage', title: '使用' },
|
|
33
|
+
{ key: 'faq', title: 'FAQ' },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const Playground: Story = {
|
|
37
|
+
parameters: { controls: { disable: true } },
|
|
38
|
+
render: () => (
|
|
39
|
+
<div className="flex gap-6">
|
|
40
|
+
<aside className="sticky top-0 h-fit w-44 shrink-0">
|
|
41
|
+
<Anchor items={items} offsetTop={20} />
|
|
42
|
+
</aside>
|
|
43
|
+
<article className="flex-1 space-y-12 text-sm">
|
|
44
|
+
<section id="intro">
|
|
45
|
+
<h2 className="mb-2 text-lg font-semibold">介绍</h2>
|
|
46
|
+
<p>{Array(8).fill('这是介绍段落。').join(' ')}</p>
|
|
47
|
+
</section>
|
|
48
|
+
<section id="install">
|
|
49
|
+
<h2 className="mb-2 text-lg font-semibold">安装</h2>
|
|
50
|
+
<section id="npm" className="mb-6">
|
|
51
|
+
<h3 className="mb-2 font-semibold">npm</h3>
|
|
52
|
+
<p>{Array(6).fill('npm install.').join(' ')}</p>
|
|
53
|
+
</section>
|
|
54
|
+
<section id="pnpm" className="mb-6">
|
|
55
|
+
<h3 className="mb-2 font-semibold">pnpm</h3>
|
|
56
|
+
<p>{Array(6).fill('pnpm add.').join(' ')}</p>
|
|
57
|
+
</section>
|
|
58
|
+
<section id="yarn">
|
|
59
|
+
<h3 className="mb-2 font-semibold">yarn</h3>
|
|
60
|
+
<p>{Array(6).fill('yarn add.').join(' ')}</p>
|
|
61
|
+
</section>
|
|
62
|
+
</section>
|
|
63
|
+
<section id="usage">
|
|
64
|
+
<h2 className="mb-2 text-lg font-semibold">使用</h2>
|
|
65
|
+
<p>{Array(10).fill('使用说明文字。').join(' ')}</p>
|
|
66
|
+
</section>
|
|
67
|
+
<section id="faq">
|
|
68
|
+
<h2 className="mb-2 text-lg font-semibold">FAQ</h2>
|
|
69
|
+
<p>{Array(10).fill('常见问题。').join(' ')}</p>
|
|
70
|
+
</section>
|
|
71
|
+
</article>
|
|
72
|
+
</div>
|
|
73
|
+
),
|
|
74
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/utils/cn';
|
|
4
|
+
|
|
5
|
+
export interface AnchorItem {
|
|
6
|
+
/** 目标元素 id(组件会按 `#${id}` 滚动到该元素)。 */
|
|
7
|
+
key: string;
|
|
8
|
+
/** 显示文本。 */
|
|
9
|
+
title: React.ReactNode;
|
|
10
|
+
/** 子节点(渲染为嵌套缩进)。 */
|
|
11
|
+
children?: AnchorItem[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AnchorProps
|
|
15
|
+
extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
|
|
16
|
+
/** 锚点项树(antd `items` 并集)。 */
|
|
17
|
+
items: AnchorItem[];
|
|
18
|
+
/**
|
|
19
|
+
* 当目标元素距视口顶部小于此值时高亮(IntersectionObserver `rootMargin`)。
|
|
20
|
+
* @default 0
|
|
21
|
+
*/
|
|
22
|
+
offsetTop?: number;
|
|
23
|
+
/**
|
|
24
|
+
* 点击锚点平滑滚动行为。
|
|
25
|
+
* @default "smooth"
|
|
26
|
+
*/
|
|
27
|
+
scrollBehavior?: 'smooth' | 'auto';
|
|
28
|
+
/** 当前高亮变化回调。 */
|
|
29
|
+
onChange?: (key: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function flatten(items: AnchorItem[]): AnchorItem[] {
|
|
33
|
+
const out: AnchorItem[] = [];
|
|
34
|
+
const walk = (it: AnchorItem) => {
|
|
35
|
+
out.push(it);
|
|
36
|
+
it.children?.forEach(walk);
|
|
37
|
+
};
|
|
38
|
+
items.forEach(walk);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 锚点导航 — antd 独有补足。**等价 antd `Anchor`**。长文档 / 详情页侧边的目录,
|
|
44
|
+
* 点击锚点滚动到对应 `id` 节点,滚动时自动高亮当前可见的锚点(IntersectionObserver)。
|
|
45
|
+
*/
|
|
46
|
+
const Anchor = React.forwardRef<HTMLElement, AnchorProps>(
|
|
47
|
+
(
|
|
48
|
+
{
|
|
49
|
+
items,
|
|
50
|
+
offsetTop = 0,
|
|
51
|
+
scrollBehavior = 'smooth',
|
|
52
|
+
onChange,
|
|
53
|
+
className,
|
|
54
|
+
...props
|
|
55
|
+
},
|
|
56
|
+
ref,
|
|
57
|
+
) => {
|
|
58
|
+
const [active, setActive] = React.useState<string>('');
|
|
59
|
+
const flat = React.useMemo(() => flatten(items), [items]);
|
|
60
|
+
|
|
61
|
+
React.useEffect(() => {
|
|
62
|
+
if (typeof window === 'undefined') return;
|
|
63
|
+
const observer = new IntersectionObserver(
|
|
64
|
+
(entries) => {
|
|
65
|
+
const visible = entries.find((e) => e.isIntersecting);
|
|
66
|
+
if (visible) {
|
|
67
|
+
const id = visible.target.id;
|
|
68
|
+
setActive(id);
|
|
69
|
+
onChange?.(id);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
rootMargin: `-${offsetTop}px 0px -50% 0px`,
|
|
74
|
+
threshold: 0,
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
flat.forEach((it) => {
|
|
78
|
+
const el = document.getElementById(it.key);
|
|
79
|
+
if (el) observer.observe(el);
|
|
80
|
+
});
|
|
81
|
+
return () => observer.disconnect();
|
|
82
|
+
}, [flat, offsetTop, onChange]);
|
|
83
|
+
|
|
84
|
+
const handleClick = (e: React.MouseEvent, key: string) => {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
const el = document.getElementById(key);
|
|
87
|
+
if (el) {
|
|
88
|
+
const top = el.getBoundingClientRect().top + window.scrollY - offsetTop;
|
|
89
|
+
window.scrollTo({ top, behavior: scrollBehavior });
|
|
90
|
+
setActive(key);
|
|
91
|
+
onChange?.(key);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const renderItem = (it: AnchorItem, depth: number): React.ReactNode => (
|
|
96
|
+
<li key={it.key}>
|
|
97
|
+
<a
|
|
98
|
+
href={`#${it.key}`}
|
|
99
|
+
onClick={(e) => handleClick(e, it.key)}
|
|
100
|
+
className={cn(
|
|
101
|
+
'block border-l-2 py-1 pr-2 text-sm transition-colors',
|
|
102
|
+
depth === 0 ? 'pl-3' : depth === 1 ? 'pl-6' : 'pl-9',
|
|
103
|
+
active === it.key
|
|
104
|
+
? 'border-primary text-primary'
|
|
105
|
+
: 'border-transparent text-muted-foreground hover:text-foreground',
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
{it.title}
|
|
109
|
+
</a>
|
|
110
|
+
{it.children && it.children.length > 0 ? (
|
|
111
|
+
<ul>{it.children.map((c) => renderItem(c, depth + 1))}</ul>
|
|
112
|
+
) : null}
|
|
113
|
+
</li>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<nav
|
|
118
|
+
ref={ref}
|
|
119
|
+
aria-label="目录"
|
|
120
|
+
className={cn('text-sm', className)}
|
|
121
|
+
{...props}
|
|
122
|
+
>
|
|
123
|
+
<ul>{items.map((it) => renderItem(it, 0))}</ul>
|
|
124
|
+
</nav>
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
Anchor.displayName = 'Anchor';
|
|
129
|
+
|
|
130
|
+
export { Anchor };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: app
|
|
3
|
+
name: App
|
|
4
|
+
type: component
|
|
5
|
+
category: shell
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# App
|
|
11
|
+
|
|
12
|
+
应用根容器 — antd 独有补足。**对标 antd `App`** 的核心职责:挂全局 Toaster + 设置 dir / lang + 承载根级 className。**与 antd 区别**:不接管主题(本库主题靠 design tokens / CSS vars,**不引入 runtime ConfigProvider**),也不收敛 `useApp()` hook(toast / notification 直接 import 即可)。
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- 整个应用的最外层容器(`layout.tsx` / `_app.tsx` / `main.tsx` root)
|
|
17
|
+
- 需要"挂一次 Toaster,后续到处 toast"的场景
|
|
18
|
+
- 国际化需要 `dir="rtl"` 时
|
|
19
|
+
|
|
20
|
+
## When NOT to use
|
|
21
|
+
|
|
22
|
+
- 子页面层级容器 → 用 `Flex` / `Grid` / 普通 div
|
|
23
|
+
- 需要 runtime 切换主题色 → 改 CSS variables(design tokens),**不要**通过 ConfigProvider
|
|
24
|
+
- 单独需要 Toaster → 直接挂 `<Toaster />`
|
|
25
|
+
|
|
26
|
+
<!-- auto:props:begin -->
|
|
27
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
28
|
+
| --- | --- | --- | --- | --- |
|
|
29
|
+
| `withToaster` | `boolean` | `true` | – | 是否挂载 `<Toaster />`(antd `App` 内部托管 message / notification 静态实例) — 全应用根挂一次。 |
|
|
30
|
+
| `toasterProps` | `ToasterProps` | – | – | 传递给 Toaster 的配置(位置 / 主题 / 自动关闭等) — 仅 `withToaster=true` 时生效。 |
|
|
31
|
+
| `dir` | `'ltr' \| 'rtl'` | `"ltr"` | – | 文字方向 — 设到根容器的 `dir` 属性,Radix 等组件会自动适配。 |
|
|
32
|
+
<!-- auto:props:end -->
|
|
33
|
+
|
|
34
|
+
<!-- auto:deps:begin -->
|
|
35
|
+
### 同库依赖
|
|
36
|
+
|
|
37
|
+
> `teamix-evo ui add app` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
38
|
+
|
|
39
|
+
| Entry | 类型 | 描述 |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
42
|
+
| `sonner` | component | Toast 通知 — sonner 包装,等价 antd message + notification 并集(toast() 函数式 API + Toaster 容器) |
|
|
43
|
+
|
|
44
|
+
### npm 依赖
|
|
45
|
+
|
|
46
|
+
_无 — 本组件不依赖任何 npm 包。_
|
|
47
|
+
<!-- auto:deps:end -->
|
|
48
|
+
|
|
49
|
+
## AI 生成纪律
|
|
50
|
+
|
|
51
|
+
- **整应用挂一次** — 子级别再嵌 `App` 会造成 Toaster 重复渲染
|
|
52
|
+
- **不要把它当 Layout 用** — 它只是根 mount 点,真正的页面布局用 `Flex` / `Grid`
|
|
53
|
+
- **`withToaster=false`** 仅当你已经在别处单独挂了 `<Toaster />` 时关闭,避免重复
|
|
54
|
+
- **`dir="rtl"`** 仅在阿语 / 希伯来语等右到左语言场景设;Radix 组件已自动适配
|
|
55
|
+
- **不要在 App 内做主题切换**:换 `class="dark"` 或写 CSS var 覆盖,**不要**走 React state
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { App } from '@/components/ui/app';
|
|
61
|
+
|
|
62
|
+
// 应用根
|
|
63
|
+
function Root() {
|
|
64
|
+
return (
|
|
65
|
+
<App>
|
|
66
|
+
<Header />
|
|
67
|
+
<Main />
|
|
68
|
+
<Footer />
|
|
69
|
+
</App>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 自定义 Toaster 位置
|
|
74
|
+
<App toasterProps={{ position: 'top-center', richColors: true }}>
|
|
75
|
+
...
|
|
76
|
+
</App>
|
|
77
|
+
|
|
78
|
+
// RTL
|
|
79
|
+
<App dir="rtl">...</App>
|
|
80
|
+
|
|
81
|
+
// 已自挂 Toaster
|
|
82
|
+
<App withToaster={false}>
|
|
83
|
+
<Toaster position="bottom-right" />
|
|
84
|
+
...
|
|
85
|
+
</App>
|
|
86
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { App } from './app';
|
|
3
|
+
import { Button } from '@/components/button/button';
|
|
4
|
+
import { toast } from '@/components/sonner/sonner';
|
|
5
|
+
import { notification } from '@/components/notification/notification';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof App> = {
|
|
8
|
+
title: '应用壳 · Shell/App',
|
|
9
|
+
component: App,
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
parameters: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
'应用根容器 — 挂全局 Toaster + 设置 dir / lang + 承载根级 className。对标 antd `App`,但不接管主题(主题靠 design tokens / CSS vars),也不收敛 useApp() hook。视觉走 OpenTrek tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
argTypes: {
|
|
20
|
+
withToaster: { control: 'boolean' },
|
|
21
|
+
dir: { control: 'inline-radio', options: ['ltr', 'rtl'] },
|
|
22
|
+
},
|
|
23
|
+
args: { withToaster: true, dir: 'ltr' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof App>;
|
|
28
|
+
|
|
29
|
+
export const Playground: Story = {
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<App {...args} className="rounded-md border p-6">
|
|
32
|
+
<div className="flex flex-col gap-3">
|
|
33
|
+
<span className="text-sm text-muted-foreground">App 内挂载了 Toaster,可直接触发:</span>
|
|
34
|
+
<div className="flex gap-2">
|
|
35
|
+
<Button onClick={() => toast.success('已保存')}>触发 toast</Button>
|
|
36
|
+
<Button
|
|
37
|
+
variant="outline"
|
|
38
|
+
onClick={() =>
|
|
39
|
+
notification.info({
|
|
40
|
+
title: 'App 容器在生效',
|
|
41
|
+
description: '右上角 Toaster 是 App 默认挂载的',
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
触发 notification
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</App>
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const RTL: Story = {
|
|
54
|
+
parameters: { controls: { disable: true } },
|
|
55
|
+
render: () => (
|
|
56
|
+
<App dir="rtl" className="rounded-md border p-6">
|
|
57
|
+
<div className="text-sm">
|
|
58
|
+
当前方向: <strong>rtl</strong> — 你会看到自然的右到左流向
|
|
59
|
+
</div>
|
|
60
|
+
</App>
|
|
61
|
+
),
|
|
62
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/utils/cn';
|
|
4
|
+
import { Toaster, type ToasterProps } from '@/components/sonner/sonner';
|
|
5
|
+
|
|
6
|
+
export interface AppProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
/**
|
|
8
|
+
* 是否挂载 `<Toaster />`(antd `App` 内部托管 message / notification 静态实例) — 全应用根挂一次。
|
|
9
|
+
* @default true
|
|
10
|
+
*/
|
|
11
|
+
withToaster?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* 传递给 Toaster 的配置(位置 / 主题 / 自动关闭等) — 仅 `withToaster=true` 时生效。
|
|
14
|
+
*/
|
|
15
|
+
toasterProps?: ToasterProps;
|
|
16
|
+
/**
|
|
17
|
+
* 文字方向 — 设到根容器的 `dir` 属性,Radix 等组件会自动适配。
|
|
18
|
+
* @default "ltr"
|
|
19
|
+
*/
|
|
20
|
+
dir?: 'ltr' | 'rtl';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 应用根容器 — antd 独有补足。**对标 antd `App`** 的核心职责:
|
|
25
|
+
* - **挂载全局 Toaster**(后续 toast / notification 静态调用的渲染层)
|
|
26
|
+
* - **设置 dir / lang** 等顶层属性
|
|
27
|
+
* - **承载根级 className**(让业务侧把 design tokens 挂到这一层即可)
|
|
28
|
+
*
|
|
29
|
+
* 与 antd 区别:
|
|
30
|
+
* - 不接管主题(本库主题靠 design tokens / CSS vars,**不引入 runtime ConfigProvider**)
|
|
31
|
+
* - 不收敛 `useApp()` hook 提供 message/notification(直接 import `toast` / `notification` 即可)
|
|
32
|
+
*/
|
|
33
|
+
const App = React.forwardRef<HTMLDivElement, AppProps>(
|
|
34
|
+
(
|
|
35
|
+
{
|
|
36
|
+
withToaster = true,
|
|
37
|
+
toasterProps,
|
|
38
|
+
dir = 'ltr',
|
|
39
|
+
className,
|
|
40
|
+
children,
|
|
41
|
+
...props
|
|
42
|
+
},
|
|
43
|
+
ref,
|
|
44
|
+
) => (
|
|
45
|
+
<div
|
|
46
|
+
ref={ref}
|
|
47
|
+
dir={dir}
|
|
48
|
+
className={cn('min-h-screen bg-background text-foreground', className)}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
{withToaster ? <Toaster {...toasterProps} /> : null}
|
|
53
|
+
</div>
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
App.displayName = 'App';
|
|
57
|
+
|
|
58
|
+
export { App };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: aspect-ratio
|
|
3
|
+
name: AspectRatio
|
|
4
|
+
type: component
|
|
5
|
+
category: foundation
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# AspectRatio
|
|
11
|
+
|
|
12
|
+
容器保持宽高比 — 基于 `@radix-ui/react-aspect-ratio`,内部用 padding-bottom 技巧实现,**无 layout shift**。
|
|
13
|
+
shadcn-only,antd 无对标。
|
|
14
|
+
|
|
15
|
+
## When to use
|
|
16
|
+
|
|
17
|
+
- 图片占位(`<img>` 加载完成前避免布局抖动)
|
|
18
|
+
- 视频 / iframe 容器(YouTube 16:9、TikTok 9:16)
|
|
19
|
+
- 卡片封面图固定比例
|
|
20
|
+
|
|
21
|
+
## When NOT to use
|
|
22
|
+
|
|
23
|
+
- 内容尺寸已知 → 直接 `width` + `height`
|
|
24
|
+
- 比例随父容器变化 → 用 `aspect-*` Tailwind utility
|
|
25
|
+
|
|
26
|
+
## Props
|
|
27
|
+
|
|
28
|
+
> 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成。
|
|
29
|
+
|
|
30
|
+
<!-- auto:props:begin -->
|
|
31
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
32
|
+
| --- | --- | --- | --- | --- |
|
|
33
|
+
| `ratio` | `number` | `1` | – | 宽高比 = 宽 / 高(必传)。常见值:`16 / 9` / `4 / 3` / `1` / `9 / 16`。 |
|
|
34
|
+
<!-- auto:props:end -->
|
|
35
|
+
|
|
36
|
+
## 依赖
|
|
37
|
+
|
|
38
|
+
> 以下表格由 `pnpm --filter @teamix-evo/ui gen:meta` 自动生成,数据源是 [`manifest.json`](../../../manifest.json)。**手工编辑 marker 之间的内容会在下次生成时被覆盖**。
|
|
39
|
+
|
|
40
|
+
<!-- auto:deps:begin -->
|
|
41
|
+
### 同库依赖
|
|
42
|
+
|
|
43
|
+
_无 — 本组件不依赖其他 ui entry。_
|
|
44
|
+
|
|
45
|
+
### npm 依赖
|
|
46
|
+
|
|
47
|
+
> 业务侧需要先 `pnpm add` / `npm install` 这些包。CLI 在 `ui add` 完成后会列出此提示。
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm add @radix-ui/react-aspect-ratio@^1.1.0
|
|
51
|
+
```
|
|
52
|
+
<!-- auto:deps:end -->
|
|
53
|
+
|
|
54
|
+
> 透传所有 `<div>` 原生属性。`ratio: number` 是最关键的 prop(必传),如 `16 / 9` / `4 / 3` / `1`。
|
|
55
|
+
|
|
56
|
+
## AI 生成纪律
|
|
57
|
+
|
|
58
|
+
- **`ratio` 必传**:不传时默认 1,但显式声明语义更清晰
|
|
59
|
+
- **子元素用绝对定位**:Radix 内部用 `position: relative`,子元素需 `absolute inset-0` 才能填满
|
|
60
|
+
- **不要嵌套**:嵌套 AspectRatio 会让父级失效(都基于 padding-bottom)
|
|
61
|
+
|
|
62
|
+
## Examples
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { AspectRatio } from '@/components/ui/aspect-ratio';
|
|
66
|
+
|
|
67
|
+
// 16:9 图片
|
|
68
|
+
<AspectRatio ratio={16 / 9}>
|
|
69
|
+
<img src="/cover.jpg" alt="" className="size-full rounded-lg object-cover" />
|
|
70
|
+
</AspectRatio>
|
|
71
|
+
|
|
72
|
+
// 1:1 头像位
|
|
73
|
+
<AspectRatio ratio={1} className="w-32">
|
|
74
|
+
<div className="size-full rounded-full bg-muted" />
|
|
75
|
+
</AspectRatio>
|
|
76
|
+
|
|
77
|
+
// YouTube 嵌入
|
|
78
|
+
<AspectRatio ratio={16 / 9}>
|
|
79
|
+
<iframe src="https://www.youtube.com/embed/..." className="size-full rounded-lg" />
|
|
80
|
+
</AspectRatio>
|
|
81
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { AspectRatio } from './aspect-ratio';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof AspectRatio> = {
|
|
5
|
+
title: '基础原语 · Foundation/AspectRatio',
|
|
6
|
+
component: AspectRatio,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
'宽高比容器 — 强制子元素按指定比例渲染,常用于图片 / 视频 / iframe 等响应式封面容器。基于 Radix AspectRatio,通过 `ratio` prop 传入任意宽高比(如 16/9、4/3、1)。shadcn 专有,填补了 antd 未提供的纯布局能力。视觉走 OpenTrek semantic tokens,所有样式来自 `@teamix-evo/design`,无 mock。',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
ratio: { control: 'number' },
|
|
18
|
+
},
|
|
19
|
+
args: { ratio: 16 / 9 },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof AspectRatio>;
|
|
24
|
+
|
|
25
|
+
export const Playground: Story = {
|
|
26
|
+
render: (args) => (
|
|
27
|
+
<div className="w-80">
|
|
28
|
+
<AspectRatio {...args} className="rounded-lg bg-muted">
|
|
29
|
+
<div className="flex size-full items-center justify-center text-sm text-muted-foreground">
|
|
30
|
+
{args.ratio?.toFixed(2)} ratio
|
|
31
|
+
</div>
|
|
32
|
+
</AspectRatio>
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Common: Story = {
|
|
38
|
+
parameters: { controls: { disable: true } },
|
|
39
|
+
render: () => (
|
|
40
|
+
<div className="grid grid-cols-3 gap-4">
|
|
41
|
+
{[
|
|
42
|
+
{ label: '16:9', ratio: 16 / 9 },
|
|
43
|
+
{ label: '4:3', ratio: 4 / 3 },
|
|
44
|
+
{ label: '1:1', ratio: 1 },
|
|
45
|
+
{ label: '3:4', ratio: 3 / 4 },
|
|
46
|
+
{ label: '9:16', ratio: 9 / 16 },
|
|
47
|
+
{ label: '21:9', ratio: 21 / 9 },
|
|
48
|
+
].map((c) => (
|
|
49
|
+
<div key={c.label} className="w-40">
|
|
50
|
+
<AspectRatio ratio={c.ratio} className="rounded-md bg-muted">
|
|
51
|
+
<div className="flex size-full items-center justify-center text-sm text-muted-foreground">
|
|
52
|
+
{c.label}
|
|
53
|
+
</div>
|
|
54
|
+
</AspectRatio>
|
|
55
|
+
</div>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
),
|
|
59
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
|
|
3
|
+
|
|
4
|
+
export interface AspectRatioProps
|
|
5
|
+
extends React.ComponentPropsWithoutRef<typeof AspectRatioPrimitive.Root> {
|
|
6
|
+
/**
|
|
7
|
+
* 宽高比 = 宽 / 高(必传)。常见值:`16 / 9` / `4 / 3` / `1` / `9 / 16`。
|
|
8
|
+
* @default 1
|
|
9
|
+
*/
|
|
10
|
+
ratio?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 容器保持指定宽高比(`ratio = width / height`)。基于 `@radix-ui/react-aspect-ratio`,
|
|
15
|
+
* 内部用 padding-bottom 技巧实现,无 layout shift。常见用于图片 / 视频 / iframe 等
|
|
16
|
+
* 内容尺寸不可控的占位场景。
|
|
17
|
+
*
|
|
18
|
+
* antd 无对标组件 — shadcn-only。
|
|
19
|
+
*/
|
|
20
|
+
const AspectRatio = AspectRatioPrimitive.Root;
|
|
21
|
+
|
|
22
|
+
export { AspectRatio };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: auto-complete
|
|
3
|
+
name: AutoComplete
|
|
4
|
+
type: component
|
|
5
|
+
category: form
|
|
6
|
+
since: 0.1.0
|
|
7
|
+
package: "@teamix-evo/ui"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# AutoComplete
|
|
11
|
+
|
|
12
|
+
输入即建议 — antd 独有补足。**等价 antd `AutoComplete`**。键入触发候选下拉,可选可改;**最终 value 可以是任意字符串**(不强制为选项之一),与 `Combobox`(必选)互补。配 `onSearch` 实现异步建议。
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
- 输入框需要"历史 / 推荐 / 自动补全"建议(搜索框、地址、邮箱补全)
|
|
17
|
+
- 候选可能很多 / 需要异步拉取
|
|
18
|
+
- 允许用户输入选项以外的自定义值
|
|
19
|
+
|
|
20
|
+
## When NOT to use
|
|
21
|
+
|
|
22
|
+
- 必选下拉(value 必须是某选项) → `Combobox` / `Select`
|
|
23
|
+
- 多选 → `Select multiple`
|
|
24
|
+
- @提及输入 → `Mentions`
|
|
25
|
+
|
|
26
|
+
<!-- auto:props:begin -->
|
|
27
|
+
| 名称 | 类型 | 默认值 | 必填 | 说明 |
|
|
28
|
+
| --- | --- | --- | --- | --- |
|
|
29
|
+
| `options` | `AutoCompleteOption[]` | – | ✓ | 候选项(antd `options` 并集) — 通常由 onSearch 回调刷新。 |
|
|
30
|
+
| `value` | `string` | – | – | 受控值。 |
|
|
31
|
+
| `defaultValue` | `string` | – | – | uncontrolled 初值。 |
|
|
32
|
+
| `onChange` | `(value: string) => void` | – | – | 值变化回调 — 用户键入 / 选中候选时触发。 |
|
|
33
|
+
| `onSearch` | `(query: string) => void` | – | – | 用户输入时回调,用于刷新 `options`(异步建议)。 |
|
|
34
|
+
| `onSelect` | `(value: string, option: AutoCompleteOption) => void` | – | – | 选中候选时回调。 |
|
|
35
|
+
| `openOnFocus` | `boolean` | `false` | – | 是否在输入框聚焦但 query 为空时显示完整 `options`(antd `defaultActiveFirstOption` 行为的近似)。 |
|
|
36
|
+
| `emptyText` | `string` | `"无匹配项"` | – | 无匹配时的提示文本。 |
|
|
37
|
+
| `size` | `'sm' \| 'default' \| 'lg'` | `"default"` | – | 容器尺寸 — 透传 Input.size。 |
|
|
38
|
+
<!-- auto:props:end -->
|
|
39
|
+
|
|
40
|
+
<!-- auto:deps:begin -->
|
|
41
|
+
### 同库依赖
|
|
42
|
+
|
|
43
|
+
> `teamix-evo ui add auto-complete` 时,以下 entry 会被自动连带安装(无需手动 add)。
|
|
44
|
+
|
|
45
|
+
| Entry | 类型 | 描述 |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `cn` | util | Tailwind className 合并工具(clsx + tailwind-merge) |
|
|
48
|
+
| `input` | component | 文本输入 — shadcn 简洁基底 + antd prefix/suffix/clearable/showCount/addonBefore/addonAfter/size |
|
|
49
|
+
|
|
50
|
+
### npm 依赖
|
|
51
|
+
|
|
52
|
+
_无 — 本组件不依赖任何 npm 包。_
|
|
53
|
+
<!-- auto:deps:end -->
|
|
54
|
+
|
|
55
|
+
## AI 生成纪律
|
|
56
|
+
|
|
57
|
+
- **`value` 是自由文本** — onChange / onSelect 都可能产生,业务侧拿到任意 string,**不要**假设它是 options 之一
|
|
58
|
+
- **`onSearch` 异步刷新**:用户键入时触发,业务侧用最新 query 拉接口刷新 `options`;**不要**在组件内做 debounce(由业务侧自行 throttle 接口)
|
|
59
|
+
- **`openOnFocus=true`** 适合"历史搜索词"场景 — 默认 false,避免空状态弹出令人困惑
|
|
60
|
+
- **键盘 ↑↓ + Enter 已实现** — 不要再监听 keydown 加自定义选择
|
|
61
|
+
- **`emptyText`** 写"业务态"文案(如"没有匹配的城市"),不要用默认的英文 / 通用占位
|
|
62
|
+
- **不要嵌套 AutoComplete** — 它本身就是组合控件,二次嵌套语义混乱
|
|
63
|
+
|
|
64
|
+
## Examples
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { AutoComplete } from '@/components/ui/auto-complete';
|
|
68
|
+
import * as React from 'react';
|
|
69
|
+
|
|
70
|
+
// 基础(静态候选)
|
|
71
|
+
<AutoComplete
|
|
72
|
+
options={[
|
|
73
|
+
{ value: 'react' },
|
|
74
|
+
{ value: 'vue' },
|
|
75
|
+
{ value: 'angular' },
|
|
76
|
+
]}
|
|
77
|
+
placeholder="输入框架名..."
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
// 异步建议
|
|
81
|
+
const [opts, setOpts] = React.useState([]);
|
|
82
|
+
<AutoComplete
|
|
83
|
+
options={opts}
|
|
84
|
+
onSearch={async (q) => {
|
|
85
|
+
const list = await searchCities(q);
|
|
86
|
+
setOpts(list.map((c) => ({ value: c.name })));
|
|
87
|
+
}}
|
|
88
|
+
placeholder="搜索城市..."
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
// 邮箱补全
|
|
92
|
+
<AutoComplete
|
|
93
|
+
options={
|
|
94
|
+
/\S+@\S*$/.test(query)
|
|
95
|
+
? []
|
|
96
|
+
: ['@gmail.com', '@outlook.com', '@qq.com'].map((d) => ({ value: query + d }))
|
|
97
|
+
}
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
// 聚焦显示历史
|
|
101
|
+
<AutoComplete openOnFocus options={historyOptions} />
|
|
102
|
+
```
|