@endfield-ui/react 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +27 -0
- package/README.md +818 -2
- package/dist/index.d.ts +3445 -0
- package/dist/index.js +8706 -0
- package/dist/styles.css +2 -0
- package/package.json +83 -3
- package/src/components/README.md +179 -0
- package/src/components/accordion/Accordion.tsx +168 -0
- package/src/components/accordion/index.ts +8 -0
- package/src/components/alert/Alert.tsx +106 -0
- package/src/components/alert/index.ts +1 -0
- package/src/components/avatar/Avatar.tsx +92 -0
- package/src/components/avatar/AvatarSwitcher.tsx +261 -0
- package/src/components/avatar/index.ts +8 -0
- package/src/components/back-to-top/BackToTop.tsx +124 -0
- package/src/components/back-to-top/index.ts +5 -0
- package/src/components/badge/Badge.tsx +75 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/bracket-title/BracketTitle.tsx +40 -0
- package/src/components/bracket-title/index.ts +1 -0
- package/src/components/breadcrumb/Breadcrumb.tsx +161 -0
- package/src/components/breadcrumb/index.ts +6 -0
- package/src/components/button/Button.tsx +357 -0
- package/src/components/button/ButtonGroup.tsx +55 -0
- package/src/components/button/index.ts +13 -0
- package/src/components/calendar/Calendar.tsx +443 -0
- package/src/components/calendar/index.ts +7 -0
- package/src/components/carousel/Carousel.tsx +252 -0
- package/src/components/carousel/index.ts +7 -0
- package/src/components/carousel/snap-track.ts +74 -0
- package/src/components/checkbox/Checkbox.tsx +94 -0
- package/src/components/checkbox/choice-style.ts +30 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/chip/FilterChip.tsx +68 -0
- package/src/components/chip/ResourceChip.tsx +63 -0
- package/src/components/chip/capsule-style.ts +22 -0
- package/src/components/chip/index.ts +6 -0
- package/src/components/combobox/Combobox.tsx +450 -0
- package/src/components/combobox/index.ts +10 -0
- package/src/components/completion-banner/CompletionBanner.tsx +72 -0
- package/src/components/completion-banner/index.ts +4 -0
- package/src/components/context-menu/ContextMenu.tsx +70 -0
- package/src/components/context-menu/index.ts +5 -0
- package/src/components/corner-brackets/CornerBrackets.tsx +43 -0
- package/src/components/corner-brackets/index.ts +5 -0
- package/src/components/countdown/Countdown.tsx +145 -0
- package/src/components/countdown/index.ts +6 -0
- package/src/components/dash-indicator/DashIndicator.tsx +44 -0
- package/src/components/dash-indicator/index.ts +1 -0
- package/src/components/data-row/DataRow.tsx +282 -0
- package/src/components/data-row/index.ts +8 -0
- package/src/components/date-picker/DateField.tsx +159 -0
- package/src/components/date-picker/DatePicker.tsx +182 -0
- package/src/components/date-picker/DateRangePicker.tsx +203 -0
- package/src/components/date-picker/index.ts +7 -0
- package/src/components/dialog/Dialog.tsx +203 -0
- package/src/components/dialog/index.ts +7 -0
- package/src/components/dialog/initial-focus.ts +19 -0
- package/src/components/dialog/overlay-style.ts +28 -0
- package/src/components/drawer/Drawer.tsx +213 -0
- package/src/components/drawer/index.ts +6 -0
- package/src/components/dropdown-menu/DropdownMenu.tsx +427 -0
- package/src/components/dropdown-menu/MenuCheck.tsx +44 -0
- package/src/components/dropdown-menu/index.ts +22 -0
- package/src/components/dropdown-menu/menu-style.ts +47 -0
- package/src/components/empty-state/EmptyState.tsx +79 -0
- package/src/components/empty-state/index.ts +1 -0
- package/src/components/field/Field.tsx +203 -0
- package/src/components/field/index.ts +6 -0
- package/src/components/file-upload/FileItem.tsx +201 -0
- package/src/components/file-upload/FileUpload.tsx +425 -0
- package/src/components/file-upload/index.ts +9 -0
- package/src/components/flyout-bar/FlyoutBar.tsx +158 -0
- package/src/components/flyout-bar/index.ts +7 -0
- package/src/components/ghost-text/GhostText.tsx +47 -0
- package/src/components/ghost-text/index.ts +5 -0
- package/src/components/hatch/Hatch.tsx +35 -0
- package/src/components/hatch/index.ts +1 -0
- package/src/components/hazard-stripe/HazardStripe.tsx +35 -0
- package/src/components/hazard-stripe/index.ts +5 -0
- package/src/components/hover-card/HoverCard.tsx +101 -0
- package/src/components/hover-card/index.ts +6 -0
- package/src/components/icon-button/IconButton.tsx +95 -0
- package/src/components/icon-button/index.ts +6 -0
- package/src/components/image-viewer/ImageViewer.tsx +427 -0
- package/src/components/image-viewer/index.ts +7 -0
- package/src/components/input/Input.tsx +109 -0
- package/src/components/input/control-box.ts +71 -0
- package/src/components/input/index.ts +6 -0
- package/src/components/input/value-chip.ts +34 -0
- package/src/components/item-slot/ItemGrid.tsx +207 -0
- package/src/components/item-slot/ItemSlot.tsx +284 -0
- package/src/components/item-slot/index.ts +7 -0
- package/src/components/kbd/Kbd.tsx +33 -0
- package/src/components/kbd/index.ts +1 -0
- package/src/components/link-element/LinkElement.tsx +25 -0
- package/src/components/list/List.tsx +229 -0
- package/src/components/list/band-style.ts +18 -0
- package/src/components/list/index.ts +8 -0
- package/src/components/loader/Loader.tsx +154 -0
- package/src/components/loader/index.ts +1 -0
- package/src/components/marquee/Marquee.tsx +96 -0
- package/src/components/marquee/index.ts +1 -0
- package/src/components/media-card/MediaCard.tsx +139 -0
- package/src/components/media-card/index.ts +5 -0
- package/src/components/nav-action/NavAction.tsx +142 -0
- package/src/components/nav-action/index.ts +5 -0
- package/src/components/nav-menu/NavMenu.tsx +288 -0
- package/src/components/nav-menu/index.ts +6 -0
- package/src/components/navigator/Navigator.tsx +136 -0
- package/src/components/navigator/index.ts +5 -0
- package/src/components/otp-input/OtpInput.tsx +211 -0
- package/src/components/otp-input/index.ts +7 -0
- package/src/components/page-header/PageHeader.tsx +182 -0
- package/src/components/page-header/index.ts +6 -0
- package/src/components/pagination/Pagination.tsx +181 -0
- package/src/components/pagination/index.ts +5 -0
- package/src/components/panel/Panel.tsx +114 -0
- package/src/components/panel/index.ts +13 -0
- package/src/components/play-button/PlayButton.tsx +92 -0
- package/src/components/play-button/index.ts +7 -0
- package/src/components/popover/Popover.tsx +97 -0
- package/src/components/popover/index.ts +8 -0
- package/src/components/popover/panel-style.ts +15 -0
- package/src/components/progress/Progress.tsx +133 -0
- package/src/components/progress/ProgressRing.tsx +123 -0
- package/src/components/progress/index.ts +2 -0
- package/src/components/radio/Radio.tsx +182 -0
- package/src/components/radio/index.ts +7 -0
- package/src/components/rec-indicator/RecIndicator.tsx +47 -0
- package/src/components/rec-indicator/index.ts +1 -0
- package/src/components/registration-strip/RegistrationStrip.tsx +51 -0
- package/src/components/registration-strip/index.ts +5 -0
- package/src/components/schedule/Schedule.tsx +457 -0
- package/src/components/schedule/index.ts +9 -0
- package/src/components/scroll-area/ScrollArea.tsx +137 -0
- package/src/components/scroll-area/index.ts +5 -0
- package/src/components/scroll-hint/ScrollHint.tsx +78 -0
- package/src/components/scroll-hint/index.ts +5 -0
- package/src/components/section-title/SectionTitle.tsx +202 -0
- package/src/components/section-title/index.ts +5 -0
- package/src/components/segmented-control/SegmentedControl.tsx +220 -0
- package/src/components/segmented-control/index.ts +8 -0
- package/src/components/select/Select.tsx +390 -0
- package/src/components/select/index.ts +15 -0
- package/src/components/side-rail/SideRail.tsx +317 -0
- package/src/components/side-rail/SideRailSub.tsx +298 -0
- package/src/components/side-rail/index.ts +14 -0
- package/src/components/skeleton/Skeleton.tsx +70 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/slider/Slider.tsx +219 -0
- package/src/components/slider/index.ts +8 -0
- package/src/components/sparkline/Sparkline.tsx +122 -0
- package/src/components/sparkline/index.ts +6 -0
- package/src/components/spinner/Spinner.tsx +64 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/stat/Stat.tsx +79 -0
- package/src/components/stat/index.ts +1 -0
- package/src/components/stepper/Stepper.tsx +316 -0
- package/src/components/stepper/index.ts +1 -0
- package/src/components/steps/Steps.tsx +302 -0
- package/src/components/steps/index.ts +10 -0
- package/src/components/switch/Switch.tsx +120 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/table/Table.tsx +560 -0
- package/src/components/table/index.ts +20 -0
- package/src/components/tabs/Tabs.tsx +313 -0
- package/src/components/tabs/index.ts +12 -0
- package/src/components/tag/Tag.tsx +66 -0
- package/src/components/tag/TagPair.tsx +34 -0
- package/src/components/tag/index.ts +2 -0
- package/src/components/tag-input/TagInput.tsx +502 -0
- package/src/components/tag-input/index.ts +8 -0
- package/src/components/term/Term.tsx +53 -0
- package/src/components/term/index.ts +1 -0
- package/src/components/textarea/Textarea.tsx +99 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/texture/Texture.tsx +45 -0
- package/src/components/texture/index.ts +1 -0
- package/src/components/tick-ring/TickRing.tsx +77 -0
- package/src/components/tick-ring/index.ts +1 -0
- package/src/components/timeline/Timeline.tsx +84 -0
- package/src/components/timeline/index.ts +7 -0
- package/src/components/toast/Toast.tsx +257 -0
- package/src/components/toast/index.ts +10 -0
- package/src/components/toc/Toc.tsx +263 -0
- package/src/components/toc/index.ts +7 -0
- package/src/components/toolbar/Toolbar.tsx +389 -0
- package/src/components/toolbar/index.ts +17 -0
- package/src/components/tooltip/Tooltip.tsx +175 -0
- package/src/components/tooltip/index.ts +8 -0
- package/src/components/top-bar/TopBar.tsx +47 -0
- package/src/components/top-bar/index.ts +1 -0
- package/src/components/viewfinder/Viewfinder.tsx +97 -0
- package/src/components/viewfinder/index.ts +6 -0
- package/src/hooks/README.md +14 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useControllableState.ts +30 -0
- package/src/hooks/useInView.ts +44 -0
- package/src/hooks/useOverflowing.ts +28 -0
- package/src/hooks/usePortalScope.ts +68 -0
- package/src/hooks/useScrollPosition.ts +57 -0
- package/src/hooks/useTheme.ts +102 -0
- package/src/icons/ArrowCorner.tsx +7 -0
- package/src/icons/ArrowLeft.tsx +6 -0
- package/src/icons/ArrowRight.tsx +6 -0
- package/src/icons/CalendarIcon.tsx +10 -0
- package/src/icons/Check.tsx +4 -0
- package/src/icons/ChevronDown.tsx +3 -0
- package/src/icons/ChevronLeft.tsx +6 -0
- package/src/icons/ChevronRight.tsx +6 -0
- package/src/icons/Close.tsx +3 -0
- package/src/icons/Crosshair.tsx +7 -0
- package/src/icons/Lock.tsx +6 -0
- package/src/icons/Menu.tsx +4 -0
- package/src/icons/Minus.tsx +4 -0
- package/src/icons/Plus.tsx +3 -0
- package/src/icons/README.md +31 -0
- package/src/icons/StatusDanger.tsx +7 -0
- package/src/icons/StatusInfo.tsx +7 -0
- package/src/icons/StatusSuccess.tsx +10 -0
- package/src/icons/StatusWarning.tsx +7 -0
- package/src/icons/TriangleRight.tsx +7 -0
- package/src/icons/Upload.tsx +7 -0
- package/src/icons/createIcon.tsx +34 -0
- package/src/icons/index.ts +21 -0
- package/src/index.ts +87 -0
- package/src/lib/README.md +17 -0
- package/src/lib/cn.ts +34 -0
- package/src/lib/date.ts +125 -0
- package/src/lib/decor.ts +6 -0
- package/src/lib/defined.ts +9 -0
- package/src/lib/file.ts +61 -0
- package/src/lib/focus-ring.ts +11 -0
- package/src/lib/merge-refs.ts +11 -0
- package/src/styles/README.md +70 -0
- package/src/styles/index.css +25 -0
- package/src/styles/theme.css +568 -0
- package/src/styles/utilities.css +241 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import { decor } from "../../lib/decor";
|
|
4
|
+
|
|
5
|
+
export type TextureVariant = "dots" | "grid" | "contour";
|
|
6
|
+
|
|
7
|
+
export type TextureProps = Omit<ComponentProps<"div">, "children"> & {
|
|
8
|
+
/**
|
|
9
|
+
* - `dots` 点阵,印刷网点:弹窗、卡片、面板的底,默认;
|
|
10
|
+
* - `grid` 工程网格,图纸与坐标:图解、工作台的背景;
|
|
11
|
+
* - `contour` 等高线,测绘与地形:偏在一角,被容器的边缘截断。
|
|
12
|
+
*/
|
|
13
|
+
variant?: TextureVariant;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const variantClass: Record<TextureVariant, string> = {
|
|
17
|
+
dots: "dot-grid",
|
|
18
|
+
grid: "blueprint-grid",
|
|
19
|
+
contour: "contour",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 铺在内容之下的一层底纹,纯装饰。默认铺满最近的定位祖先:
|
|
24
|
+
* 父容器要自己写 `relative`,需要裁切时再加 `overflow-clip`。
|
|
25
|
+
* 一个视口选一种;文字下面垫一层实色,别让底纹穿过正文。
|
|
26
|
+
*/
|
|
27
|
+
export function Texture({
|
|
28
|
+
variant = "dots",
|
|
29
|
+
className,
|
|
30
|
+
...props
|
|
31
|
+
}: TextureProps) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
{...props}
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
data-variant={variant}
|
|
37
|
+
className={cn(
|
|
38
|
+
decor,
|
|
39
|
+
"absolute inset-0",
|
|
40
|
+
variantClass[variant],
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Texture, type TextureProps, type TextureVariant } from "./Texture";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import { decor } from "../../lib/decor";
|
|
4
|
+
|
|
5
|
+
export type TickRingProps = Omit<ComponentProps<"div">, "children"> & {
|
|
6
|
+
/** 直径:数字是像素,也可以传 `"100%"` 这类长度(高度跟着宽度走)。默认 240 */
|
|
7
|
+
size?: number | string;
|
|
8
|
+
/** 外圈刻度的根数,默认 40 */
|
|
9
|
+
ticks?: number;
|
|
10
|
+
/** 整体缓慢匀速旋转(60 秒一圈) */
|
|
11
|
+
spin?: boolean;
|
|
12
|
+
/** 被圆环围住的主体,居中 */
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 刻度圆环:细内环 + 一圈刻度 + 两段不对称的粗弧,围住一个主体(模型、头像、地图焦点)。
|
|
18
|
+
* 纯装饰,只用在展示性的画面上。颜色继承文字:浅色底上是墨与灰,
|
|
19
|
+
* 压在深色画面上时给它加 `data-theme="dark"`。
|
|
20
|
+
*
|
|
21
|
+
* 它不承载数据;表示进度用 `ProgressRing`。
|
|
22
|
+
*/
|
|
23
|
+
export function TickRing({
|
|
24
|
+
size = 240,
|
|
25
|
+
ticks = 40,
|
|
26
|
+
spin = false,
|
|
27
|
+
className,
|
|
28
|
+
style,
|
|
29
|
+
children,
|
|
30
|
+
...props
|
|
31
|
+
}: TickRingProps) {
|
|
32
|
+
const count = Math.max(1, Math.round(ticks));
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
{...props}
|
|
37
|
+
className={cn(
|
|
38
|
+
"relative inline-grid aspect-square shrink-0 place-items-center text-ink",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
style={{ width: size, ...style }}
|
|
42
|
+
>
|
|
43
|
+
<svg
|
|
44
|
+
aria-hidden="true"
|
|
45
|
+
focusable="false"
|
|
46
|
+
viewBox="0 0 240 240"
|
|
47
|
+
fill="none"
|
|
48
|
+
stroke="currentColor"
|
|
49
|
+
className={cn(
|
|
50
|
+
decor,
|
|
51
|
+
"absolute inset-0 size-full",
|
|
52
|
+
spin && "animate-spin-slow",
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
<circle cx="120" cy="120" r="96" strokeOpacity={0.35} />
|
|
56
|
+
{/* pathLength 把周长归一化成"每根刻度 10 个单位":短线 1.5、间隔 8.5 */}
|
|
57
|
+
<circle
|
|
58
|
+
data-ticks={count}
|
|
59
|
+
cx="120"
|
|
60
|
+
cy="120"
|
|
61
|
+
r="114"
|
|
62
|
+
strokeOpacity={0.5}
|
|
63
|
+
strokeWidth={8}
|
|
64
|
+
pathLength={count * 10}
|
|
65
|
+
strokeDasharray="1.5 8.5"
|
|
66
|
+
/>
|
|
67
|
+
{/* 两段弧一长一短、不对称摆放:等长对称的两段会被看成加载指示 */}
|
|
68
|
+
<path
|
|
69
|
+
d="M120 6A114 114 0 0 1 218.7 63M81 227.1A114 114 0 0 1 25.5 183.7"
|
|
70
|
+
strokeOpacity={0.55}
|
|
71
|
+
strokeWidth={5}
|
|
72
|
+
/>
|
|
73
|
+
</svg>
|
|
74
|
+
{children !== undefined && <div className="relative">{children}</div>}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TickRing, type TickRingProps } from "./TickRing";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type TimelineProps = ComponentProps<"ol">;
|
|
5
|
+
|
|
6
|
+
/** 竖向时间线的容器,里面只放 `TimelineItem`,按时间顺序排。 */
|
|
7
|
+
export function Timeline({ className, ...props }: TimelineProps) {
|
|
8
|
+
return <ol {...props} className={cn("text-ink", className)} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type TimelineStatus = "past" | "current" | "upcoming";
|
|
12
|
+
|
|
13
|
+
export type TimelineItemProps = Omit<ComponentProps<"li">, "title"> & {
|
|
14
|
+
/**
|
|
15
|
+
* - `past` 已过:实心菱形,默认;
|
|
16
|
+
* - `current` 当前:黄色菱形,标题加粗;
|
|
17
|
+
* - `upcoming` 未到:空心菱形,文字降为次级色。
|
|
18
|
+
*/
|
|
19
|
+
status?: TimelineStatus;
|
|
20
|
+
/** 节点旁的日期或时间,等宽小字 */
|
|
21
|
+
date?: ReactNode;
|
|
22
|
+
title: ReactNode;
|
|
23
|
+
/** 标题下的说明 */
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const nodeClass: Record<TimelineStatus, string> = {
|
|
28
|
+
past: "bg-ink",
|
|
29
|
+
// 描一圈最细的线(1px),颜色跟着文字走:亮色下是墨色,暗色下是近白
|
|
30
|
+
current: "border border-ink bg-action",
|
|
31
|
+
upcoming: "border-2 border-ink-tertiary",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const titleClass: Record<TimelineStatus, string> = {
|
|
35
|
+
past: "text-ink",
|
|
36
|
+
current: "font-bold text-ink",
|
|
37
|
+
upcoming: "text-ink-secondary",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function TimelineItem({
|
|
41
|
+
status = "past",
|
|
42
|
+
date,
|
|
43
|
+
title,
|
|
44
|
+
className,
|
|
45
|
+
children,
|
|
46
|
+
...props
|
|
47
|
+
}: TimelineItemProps) {
|
|
48
|
+
const hasDate = date !== undefined && date !== null && date !== false;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<li
|
|
52
|
+
{...props}
|
|
53
|
+
data-status={status}
|
|
54
|
+
aria-current={status === "current" ? "step" : undefined}
|
|
55
|
+
className={cn("group/item flex gap-3", className)}
|
|
56
|
+
>
|
|
57
|
+
{/* 字号跟第一行文字走,节点才能对齐它 */}
|
|
58
|
+
<span
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
className={cn(
|
|
61
|
+
"flex shrink-0 flex-col items-center",
|
|
62
|
+
hasDate ? "text-xs" : "text-base",
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<span className="flex h-[1lh] min-h-4 w-4 items-center justify-center">
|
|
66
|
+
<span className={cn("size-[11px] rotate-45", nodeClass[status])} />
|
|
67
|
+
</span>
|
|
68
|
+
{/* 轴只画在节点之间,不从节点后面穿过,空心菱形才是空的 */}
|
|
69
|
+
<span className="w-0.5 flex-1 bg-line-strong group-last/item:hidden" />
|
|
70
|
+
</span>
|
|
71
|
+
<div className="min-w-0 flex-1 pb-6 group-last/item:pb-0">
|
|
72
|
+
{hasDate && (
|
|
73
|
+
<div className="font-tech text-xs text-ink-secondary tabular-nums">
|
|
74
|
+
{date}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
<div className={cn("wrap-anywhere", titleClass[status])}>{title}</div>
|
|
78
|
+
{children !== undefined && (
|
|
79
|
+
<div className="mt-1 text-sm text-ink-secondary">{children}</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
</li>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { Toast as BaseToast } from "@base-ui/react/toast";
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
isValidElement,
|
|
5
|
+
useContext,
|
|
6
|
+
useMemo,
|
|
7
|
+
type ComponentType,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { Close } from "../../icons/Close";
|
|
11
|
+
import type { IconProps } from "../../icons/createIcon";
|
|
12
|
+
import { StatusDanger } from "../../icons/StatusDanger";
|
|
13
|
+
import { StatusInfo } from "../../icons/StatusInfo";
|
|
14
|
+
import { StatusSuccess } from "../../icons/StatusSuccess";
|
|
15
|
+
import { StatusWarning } from "../../icons/StatusWarning";
|
|
16
|
+
import { cn } from "../../lib/cn";
|
|
17
|
+
import { focusRing, focusRingInset } from "../../lib/focus-ring";
|
|
18
|
+
|
|
19
|
+
export type ToastTone = "neutral" | "info" | "success" | "warning" | "danger";
|
|
20
|
+
export type ToastPlacement = "bottom" | "center";
|
|
21
|
+
|
|
22
|
+
export type ToastAction = {
|
|
23
|
+
label: ReactNode;
|
|
24
|
+
/** 点了之后这条提示随即关掉 */
|
|
25
|
+
onClick: () => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ToastOptions = {
|
|
29
|
+
/** 一句话 */
|
|
30
|
+
message: ReactNode;
|
|
31
|
+
/** 文字前的小图形,区分成功与失败。底色不变。默认 `neutral`:没有图形 */
|
|
32
|
+
tone?: ToastTone;
|
|
33
|
+
/** 一个操作,比如"撤销"。带操作的提示多停一会儿,并出现关闭图标 */
|
|
34
|
+
action?: ToastAction;
|
|
35
|
+
/** 停留多久,毫秒。`0` 是一直留着,直到被关掉或被下一条替换 */
|
|
36
|
+
duration?: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type ToastApi = {
|
|
40
|
+
/** 弹出一条,返回它的 id。上一条还在的话会被它替换 */
|
|
41
|
+
(input: ReactNode | ToastOptions): string;
|
|
42
|
+
/** 关掉指定的一条;不传 id 就关掉当前这条 */
|
|
43
|
+
dismiss: (id?: string) => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type ToastData = {
|
|
47
|
+
tone: ToastTone;
|
|
48
|
+
dismissible: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/* 带操作的提示默认停留的时间:要留出读完再去点的工夫 */
|
|
52
|
+
const ACTION_DURATION = 8000;
|
|
53
|
+
|
|
54
|
+
const ToastContext = createContext<ToastApi | null>(null);
|
|
55
|
+
|
|
56
|
+
/** 弹出轻提示。要在 `ToastProvider` 里面用。 */
|
|
57
|
+
export function useToast(): ToastApi {
|
|
58
|
+
const toast = useContext(ToastContext);
|
|
59
|
+
if (!toast) {
|
|
60
|
+
throw new Error("useToast 要在 <ToastProvider> 里面用");
|
|
61
|
+
}
|
|
62
|
+
return toast;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* 和提示条一样,四种色调用四种不同外形的图标 */
|
|
66
|
+
const toneIcon: Record<
|
|
67
|
+
Exclude<ToastTone, "neutral">,
|
|
68
|
+
ComponentType<IconProps>
|
|
69
|
+
> = {
|
|
70
|
+
info: StatusInfo,
|
|
71
|
+
success: StatusSuccess,
|
|
72
|
+
warning: StatusWarning,
|
|
73
|
+
danger: StatusDanger,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const toneText: Record<Exclude<ToastTone, "neutral">, string> = {
|
|
77
|
+
info: "text-info",
|
|
78
|
+
success: "text-success",
|
|
79
|
+
warning: "text-warning",
|
|
80
|
+
danger: "text-danger",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
* 水平居中 + 跟着手指走的位移(可以往下或往右划走)。
|
|
85
|
+
* 新旧两条在同一个位置交替淡入淡出,所以用绝对定位叠在一起
|
|
86
|
+
*/
|
|
87
|
+
const placementClass: Record<
|
|
88
|
+
ToastPlacement,
|
|
89
|
+
{ viewport: string; root: string }
|
|
90
|
+
> = {
|
|
91
|
+
bottom: {
|
|
92
|
+
viewport: "bottom-6",
|
|
93
|
+
root: "bottom-0 translate-y-[var(--toast-swipe-movement-y,0px)]",
|
|
94
|
+
},
|
|
95
|
+
center: {
|
|
96
|
+
viewport: "top-1/2",
|
|
97
|
+
root: "top-0 translate-y-[calc(-50%+var(--toast-swipe-movement-y,0px))]",
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function isOptions(input: ReactNode | ToastOptions): input is ToastOptions {
|
|
102
|
+
return (
|
|
103
|
+
typeof input === "object" &&
|
|
104
|
+
input !== null &&
|
|
105
|
+
!isValidElement(input) &&
|
|
106
|
+
"message" in input
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let counter = 0;
|
|
111
|
+
|
|
112
|
+
type ToastHostProps = {
|
|
113
|
+
placement: ToastPlacement;
|
|
114
|
+
label: string;
|
|
115
|
+
closeLabel: string;
|
|
116
|
+
children: ReactNode;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function ToastHost({ placement, label, closeLabel, children }: ToastHostProps) {
|
|
120
|
+
const { toasts, add, close } = BaseToast.useToastManager<ToastData>();
|
|
121
|
+
|
|
122
|
+
const api = useMemo<ToastApi>(() => {
|
|
123
|
+
const show = (input: ReactNode | ToastOptions) => {
|
|
124
|
+
const options = isOptions(input) ? input : { message: input };
|
|
125
|
+
const { message, tone = "neutral", action, duration } = options;
|
|
126
|
+
const id = `ef-toast-${++counter}`;
|
|
127
|
+
|
|
128
|
+
// 同时只显示一条:新的替换旧的
|
|
129
|
+
close();
|
|
130
|
+
return add({
|
|
131
|
+
id,
|
|
132
|
+
title: message,
|
|
133
|
+
type: tone,
|
|
134
|
+
// 警告与失败要打断读屏当前在读的内容,其余的排队
|
|
135
|
+
priority: tone === "danger" || tone === "warning" ? "high" : "low",
|
|
136
|
+
timeout: duration ?? (action ? ACTION_DURATION : undefined),
|
|
137
|
+
actionProps: action && {
|
|
138
|
+
children: action.label,
|
|
139
|
+
onClick: () => {
|
|
140
|
+
action.onClick();
|
|
141
|
+
close(id);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
data: { tone, dismissible: action !== undefined || duration === 0 },
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
show.dismiss = (id?: string) => close(id);
|
|
148
|
+
return show;
|
|
149
|
+
}, [add, close]);
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<ToastContext value={api}>
|
|
153
|
+
{children}
|
|
154
|
+
<BaseToast.Portal>
|
|
155
|
+
<BaseToast.Viewport
|
|
156
|
+
aria-label={label}
|
|
157
|
+
className={cn(
|
|
158
|
+
"pointer-events-none fixed inset-x-4 z-(--z-toast) h-0",
|
|
159
|
+
placementClass[placement].viewport,
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
{toasts.map((toast) => {
|
|
163
|
+
const tone = toast.data?.tone ?? "neutral";
|
|
164
|
+
const Icon = tone === "neutral" ? null : toneIcon[tone];
|
|
165
|
+
return (
|
|
166
|
+
<BaseToast.Root
|
|
167
|
+
key={toast.id}
|
|
168
|
+
toast={toast}
|
|
169
|
+
// 黑底白字不随主题变:整条做成暗色的局部主题,图形和焦点环按深色底取值
|
|
170
|
+
data-theme="dark"
|
|
171
|
+
className={cn(
|
|
172
|
+
"pointer-events-auto absolute left-1/2 w-max max-w-full translate-x-[calc(-50%+var(--toast-swipe-movement-x,0px))]",
|
|
173
|
+
// 边线是给暗色页面的:那里黑底和页面几乎一样深,要靠它把提示勾出来。
|
|
174
|
+
// 亮色页面上它和提示的底几乎同色,看不出来
|
|
175
|
+
"border border-line bg-black/80 text-base leading-[1.4] text-white",
|
|
176
|
+
"transition-opacity duration-(--duration-fast) ease-standard data-ending-style:opacity-0 data-starting-style:opacity-0",
|
|
177
|
+
// 焦点环画在里面:它是按深色底取的颜色,画到外面会压在亮色的页面上
|
|
178
|
+
focusRingInset,
|
|
179
|
+
placementClass[placement].root,
|
|
180
|
+
)}
|
|
181
|
+
>
|
|
182
|
+
<BaseToast.Content
|
|
183
|
+
className={cn(
|
|
184
|
+
"flex items-center gap-3 py-2.5 pl-5",
|
|
185
|
+
toast.data?.dismissible ? "pr-2" : "pr-5",
|
|
186
|
+
)}
|
|
187
|
+
>
|
|
188
|
+
{Icon && tone !== "neutral" && (
|
|
189
|
+
<Icon
|
|
190
|
+
size={16}
|
|
191
|
+
className={cn("shrink-0", toneText[tone])}
|
|
192
|
+
/>
|
|
193
|
+
)}
|
|
194
|
+
<BaseToast.Title
|
|
195
|
+
render={<p />}
|
|
196
|
+
className="min-w-0 wrap-anywhere"
|
|
197
|
+
/>
|
|
198
|
+
{toast.actionProps && (
|
|
199
|
+
<BaseToast.Action
|
|
200
|
+
className={cn(
|
|
201
|
+
"shrink-0 border-b border-current text-sm font-medium text-accent-ink active:opacity-80",
|
|
202
|
+
focusRing,
|
|
203
|
+
)}
|
|
204
|
+
/>
|
|
205
|
+
)}
|
|
206
|
+
{toast.data?.dismissible && (
|
|
207
|
+
<BaseToast.Close
|
|
208
|
+
aria-label={closeLabel}
|
|
209
|
+
className={cn(
|
|
210
|
+
"inline-flex size-8 shrink-0 items-center justify-center text-ink-secondary hover:text-ink",
|
|
211
|
+
focusRing,
|
|
212
|
+
)}
|
|
213
|
+
>
|
|
214
|
+
<Close size={16} />
|
|
215
|
+
</BaseToast.Close>
|
|
216
|
+
)}
|
|
217
|
+
</BaseToast.Content>
|
|
218
|
+
</BaseToast.Root>
|
|
219
|
+
);
|
|
220
|
+
})}
|
|
221
|
+
</BaseToast.Viewport>
|
|
222
|
+
</BaseToast.Portal>
|
|
223
|
+
</ToastContext>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export type ToastProviderProps = {
|
|
228
|
+
/** 出现在哪:视口底部居中(默认),或者视口正中(官网的做法) */
|
|
229
|
+
placement?: ToastPlacement;
|
|
230
|
+
/** 默认停留多久,毫秒。规范是 2 – 4 秒 */
|
|
231
|
+
duration?: number;
|
|
232
|
+
/** 提示区域的可访问名称(按 `F6` 可以把焦点移进来) */
|
|
233
|
+
label?: string;
|
|
234
|
+
/** 关闭图标的可访问名称 */
|
|
235
|
+
closeLabel?: string;
|
|
236
|
+
children: ReactNode;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 轻提示的容器,包在应用的最外层。里面的任何地方用 `useToast()` 弹出。
|
|
241
|
+
* 重要的、需要用户处理的信息不要用轻提示,用提示条或弹窗。
|
|
242
|
+
*/
|
|
243
|
+
export function ToastProvider({
|
|
244
|
+
placement = "bottom",
|
|
245
|
+
duration = 3000,
|
|
246
|
+
label = "通知",
|
|
247
|
+
closeLabel = "关闭",
|
|
248
|
+
children,
|
|
249
|
+
}: ToastProviderProps) {
|
|
250
|
+
return (
|
|
251
|
+
<BaseToast.Provider timeout={duration} limit={1}>
|
|
252
|
+
<ToastHost placement={placement} label={label} closeLabel={closeLabel}>
|
|
253
|
+
{children}
|
|
254
|
+
</ToastHost>
|
|
255
|
+
</BaseToast.Provider>
|
|
256
|
+
);
|
|
257
|
+
}
|