@endfield-ui/react 0.0.0-stage → 0.1.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 +768 -2
- package/dist/index.d.ts +3243 -0
- package/dist/index.js +8182 -0
- package/dist/styles.css +2 -0
- package/package.json +83 -3
- package/src/components/README.md +172 -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 +89 -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 +292 -0
- package/src/components/carousel/index.ts +7 -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/icon-button/IconButton.tsx +95 -0
- package/src/components/icon-button/index.ts +6 -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 +283 -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/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 +105 -0
- package/src/components/popover/index.ts +8 -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-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 +559 -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 +501 -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 +262 -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 +82 -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,114 @@
|
|
|
1
|
+
import type { ComponentProps, ElementType, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type PanelProps = ComponentProps<"div"> & {
|
|
5
|
+
/** 1px 描边,默认有。关掉后只靠底色差分层 */
|
|
6
|
+
bordered?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** 围住一组信息的容器:直角、底色只比页面差一档、没有阴影。不要面板套面板。 */
|
|
10
|
+
export function Panel({ bordered = true, className, ...props }: PanelProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
{...props}
|
|
14
|
+
className={cn(
|
|
15
|
+
"bg-surface-raised text-ink",
|
|
16
|
+
bordered && "border border-line",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type PanelHeaderVariant = "band" | "line";
|
|
24
|
+
|
|
25
|
+
export type PanelHeaderProps = Omit<ComponentProps<"div">, "title"> & {
|
|
26
|
+
/**
|
|
27
|
+
* - `band` 反转的横带 + 左端短竖条,默认。它是一个 `data-theme="inverse"` 的
|
|
28
|
+
* 局部主题:里面的按钮、标签、焦点环都按这条带子的底色取值;
|
|
29
|
+
* - `line` 只有一条细线和小标题,更轻。同一个界面选一种。
|
|
30
|
+
*/
|
|
31
|
+
variant?: PanelHeaderVariant;
|
|
32
|
+
/** 标题右侧的计数或一个小操作 */
|
|
33
|
+
extra?: ReactNode;
|
|
34
|
+
/** 标题层级,默认 3(`<h3>`) */
|
|
35
|
+
level?: 2 | 3 | 4 | 5 | 6;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function PanelHeader({
|
|
39
|
+
variant = "band",
|
|
40
|
+
extra,
|
|
41
|
+
level = 3,
|
|
42
|
+
className,
|
|
43
|
+
children,
|
|
44
|
+
...props
|
|
45
|
+
}: PanelHeaderProps) {
|
|
46
|
+
const Heading: ElementType = `h${level}`;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
// 标题带和页面相反:整条换成反转主题,下面照常用 surface / ink
|
|
51
|
+
data-theme={variant === "band" ? "inverse" : undefined}
|
|
52
|
+
{...props}
|
|
53
|
+
className={cn(
|
|
54
|
+
"relative flex items-center justify-between gap-3 px-4 text-sm font-medium text-ink",
|
|
55
|
+
variant === "band"
|
|
56
|
+
? "min-h-8 bg-surface"
|
|
57
|
+
: "min-h-10 border-b border-line",
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
{variant === "band" && (
|
|
62
|
+
// 亮色页面上是黄条压墨底,暗色页面上是深黄条压近白底
|
|
63
|
+
<span
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
className="absolute top-1/2 left-0 h-4 w-1 -translate-y-1/2 bg-accent-ink"
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
<Heading className="min-w-0 truncate">{children}</Heading>
|
|
69
|
+
{extra && <div className="flex shrink-0 items-center gap-2">{extra}</div>}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type PanelBodyProps = ComponentProps<"div">;
|
|
75
|
+
|
|
76
|
+
export function PanelBody({ className, ...props }: PanelBodyProps) {
|
|
77
|
+
return <div {...props} className={cn("p-4", className)} />;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type PanelRowsProps = ComponentProps<"dl">;
|
|
81
|
+
|
|
82
|
+
/** 属性行的容器。里面只放 `PanelRow`。 */
|
|
83
|
+
export function PanelRows({ className, ...props }: PanelRowsProps) {
|
|
84
|
+
return <dl {...props} className={cn("px-4", className)} />;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type PanelRowProps = Omit<ComponentProps<"div">, "children"> & {
|
|
88
|
+
/** 属性名,左对齐 */
|
|
89
|
+
label: ReactNode;
|
|
90
|
+
/** 属性值,右对齐、等宽、加粗 */
|
|
91
|
+
children: ReactNode;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export function PanelRow({
|
|
95
|
+
label,
|
|
96
|
+
children,
|
|
97
|
+
className,
|
|
98
|
+
...props
|
|
99
|
+
}: PanelRowProps) {
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
{...props}
|
|
103
|
+
className={cn(
|
|
104
|
+
"flex items-baseline justify-between gap-4 border-b border-line py-2.5 last:border-b-0",
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
<dt className="text-sm text-ink-secondary">{label}</dt>
|
|
109
|
+
<dd className="text-right font-tech text-sm font-bold text-ink tabular-nums">
|
|
110
|
+
{children}
|
|
111
|
+
</dd>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { TriangleRight } from "../../icons/TriangleRight";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { focusRing } from "../../lib/focus-ring";
|
|
5
|
+
|
|
6
|
+
export type PlayMarkSize = "sm" | "md" | "lg";
|
|
7
|
+
|
|
8
|
+
/* 24 / 32 / 40px,三角占一半 */
|
|
9
|
+
const boxSize: Record<PlayMarkSize, string> = {
|
|
10
|
+
sm: "size-6 [&_svg]:size-3",
|
|
11
|
+
md: "size-8 [&_svg]:size-4",
|
|
12
|
+
lg: "size-10 [&_svg]:size-5",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* 黄色小方块 + 墨色三角。有意不随主题变:它压在图上。
|
|
17
|
+
* 黄色压在浅色的图上边界不清楚,认它靠的是里面的三角。
|
|
18
|
+
*/
|
|
19
|
+
const markBase =
|
|
20
|
+
"inline-flex shrink-0 items-center justify-center rounded-xs bg-action text-on-action";
|
|
21
|
+
|
|
22
|
+
export type PlayMarkProps = Omit<ComponentProps<"span">, "children"> & {
|
|
23
|
+
size?: PlayMarkSize;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 播放记号:只是一个记号,不能点,对读屏隐藏。
|
|
28
|
+
* 整张卡已经是一个链接时用它(媒体卡的 `video`);要它自己能点用 `PlayButton`。
|
|
29
|
+
*/
|
|
30
|
+
export function PlayMark({ size = "md", className, ...props }: PlayMarkProps) {
|
|
31
|
+
return (
|
|
32
|
+
<span
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
{...props}
|
|
35
|
+
data-size={size}
|
|
36
|
+
className={cn(markBase, boxSize[size], className)}
|
|
37
|
+
>
|
|
38
|
+
<TriangleRight />
|
|
39
|
+
</span>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type PlayButtonProps = Omit<
|
|
44
|
+
ComponentProps<"button">,
|
|
45
|
+
"children" | "aria-label"
|
|
46
|
+
> & {
|
|
47
|
+
/** 默认"播放"。一页有好几个时写明播的是什么 */
|
|
48
|
+
"aria-label"?: string;
|
|
49
|
+
size?: PlayMarkSize;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** 播放钮:和播放记号同一个画法的按钮,给"点了就在原地播放"的封面用。 */
|
|
53
|
+
export function PlayButton({
|
|
54
|
+
"aria-label": ariaLabel = "播放",
|
|
55
|
+
size = "md",
|
|
56
|
+
type = "button",
|
|
57
|
+
disabled = false,
|
|
58
|
+
className,
|
|
59
|
+
...props
|
|
60
|
+
}: PlayButtonProps) {
|
|
61
|
+
return (
|
|
62
|
+
<button
|
|
63
|
+
{...props}
|
|
64
|
+
type={type}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
aria-label={ariaLabel}
|
|
67
|
+
data-size={size}
|
|
68
|
+
className={cn(
|
|
69
|
+
"group relative",
|
|
70
|
+
markBase,
|
|
71
|
+
boxSize[size],
|
|
72
|
+
"transition-colors duration-(--duration-fast) ease-standard",
|
|
73
|
+
focusRing,
|
|
74
|
+
// 24px 小于触屏的最小点击区,用伪元素向外补到 40px
|
|
75
|
+
size === "sm" && "after:absolute after:-inset-2 after:content-['']",
|
|
76
|
+
disabled
|
|
77
|
+
? "cursor-not-allowed bg-disabled text-on-disabled"
|
|
78
|
+
: "active:bg-action-pressed",
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{/* 悬停:记号让一步,和按钮的竖条变箭头是同一种做法 */}
|
|
83
|
+
<TriangleRight
|
|
84
|
+
className={cn(
|
|
85
|
+
"transition-[translate] duration-(--duration-base) ease-standard",
|
|
86
|
+
!disabled &&
|
|
87
|
+
"group-hover:translate-x-0.5 group-active:translate-x-0.5",
|
|
88
|
+
)}
|
|
89
|
+
/>
|
|
90
|
+
</button>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Popover as BasePopover } from "@base-ui/react/popover";
|
|
2
|
+
import type { ComponentProps, ReactElement, ReactNode } from "react";
|
|
3
|
+
import { usePortalScope } from "../../hooks/usePortalScope";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import { menuPanel } from "../dropdown-menu/menu-style";
|
|
6
|
+
|
|
7
|
+
export type PopoverSide = "top" | "bottom" | "left" | "right";
|
|
8
|
+
export type PopoverAlign = "start" | "center" | "end";
|
|
9
|
+
|
|
10
|
+
export type PopoverProps = Omit<
|
|
11
|
+
ComponentProps<"div">,
|
|
12
|
+
"title" | "children" | "className" | "style"
|
|
13
|
+
> & {
|
|
14
|
+
/** 触发按钮 */
|
|
15
|
+
trigger: ReactElement<Record<string, unknown>>;
|
|
16
|
+
/** 标题。有它时它就是面板的可访问名称;没有时自己传 `aria-label` */
|
|
17
|
+
title?: ReactNode;
|
|
18
|
+
/** 标题下的一行说明,会关联给读屏 */
|
|
19
|
+
description?: ReactNode;
|
|
20
|
+
/** 面板出现在触发按钮的哪一侧,放不下时自动翻到对面。默认 `bottom` */
|
|
21
|
+
side?: PopoverSide;
|
|
22
|
+
/** 和触发按钮的哪一边对齐。默认 `start` */
|
|
23
|
+
align?: PopoverAlign;
|
|
24
|
+
open?: boolean;
|
|
25
|
+
defaultOpen?: boolean;
|
|
26
|
+
onOpenChange?: (open: boolean) => void;
|
|
27
|
+
/** 给面板 */
|
|
28
|
+
className?: string;
|
|
29
|
+
children?: ReactNode;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 气泡卡片:点击触发、里面可以操作的一小块面板。不是模态——页面其余部分照常可用,
|
|
34
|
+
* 点别处、按 `Esc`、焦点移出去都会关。只是一句说明用 `Tooltip`,
|
|
35
|
+
* 必须做完才能继续的用 `Dialog`。
|
|
36
|
+
*/
|
|
37
|
+
export function Popover({
|
|
38
|
+
trigger,
|
|
39
|
+
title,
|
|
40
|
+
description,
|
|
41
|
+
side = "bottom",
|
|
42
|
+
align = "start",
|
|
43
|
+
open,
|
|
44
|
+
defaultOpen,
|
|
45
|
+
onOpenChange,
|
|
46
|
+
className,
|
|
47
|
+
children,
|
|
48
|
+
...props
|
|
49
|
+
}: PopoverProps) {
|
|
50
|
+
const { anchorRef, portalRef } = usePortalScope();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<BasePopover.Root
|
|
54
|
+
open={open}
|
|
55
|
+
defaultOpen={defaultOpen}
|
|
56
|
+
onOpenChange={onOpenChange && ((next) => onOpenChange(next))}
|
|
57
|
+
>
|
|
58
|
+
<BasePopover.Trigger ref={anchorRef} render={trigger} />
|
|
59
|
+
<BasePopover.Portal ref={portalRef}>
|
|
60
|
+
<BasePopover.Positioner
|
|
61
|
+
side={side}
|
|
62
|
+
align={align}
|
|
63
|
+
sideOffset={4}
|
|
64
|
+
className="z-(--z-overlay) outline-none"
|
|
65
|
+
>
|
|
66
|
+
<BasePopover.Popup
|
|
67
|
+
{...props}
|
|
68
|
+
className={cn(
|
|
69
|
+
menuPanel,
|
|
70
|
+
// 面板和下拉菜单是同一份画法,只把上下 4px 的内边距换成四周 16px
|
|
71
|
+
"flex w-max max-w-[min(20rem,var(--available-width))] flex-col gap-3 p-4 text-sm wrap-anywhere",
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
{(title || description) && (
|
|
76
|
+
<div className="flex flex-col gap-1">
|
|
77
|
+
{title && (
|
|
78
|
+
<BasePopover.Title className="text-base font-bold">
|
|
79
|
+
{title}
|
|
80
|
+
</BasePopover.Title>
|
|
81
|
+
)}
|
|
82
|
+
{description && (
|
|
83
|
+
<BasePopover.Description className="text-ink-secondary">
|
|
84
|
+
{description}
|
|
85
|
+
</BasePopover.Description>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
{children}
|
|
90
|
+
</BasePopover.Popup>
|
|
91
|
+
</BasePopover.Positioner>
|
|
92
|
+
</BasePopover.Portal>
|
|
93
|
+
</BasePopover.Root>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type PopoverCloseProps = {
|
|
98
|
+
/** 点了就关掉面板的按钮,比如"完成" */
|
|
99
|
+
children: ReactElement<Record<string, unknown>>;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** 包住面板里"点了就关"的按钮。 */
|
|
103
|
+
export function PopoverClose({ children }: PopoverCloseProps) {
|
|
104
|
+
return <BasePopover.Close render={children} />;
|
|
105
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type ProgressSize = "sm" | "md";
|
|
5
|
+
|
|
6
|
+
export type ProgressProps = Omit<ComponentProps<"div">, "children"> & {
|
|
7
|
+
/** 当前值。不传就是"不确定进度":一段色块在轨道上往返 */
|
|
8
|
+
value?: number;
|
|
9
|
+
/** 最大值,默认 100;分段进度下默认等于段数 */
|
|
10
|
+
max?: number;
|
|
11
|
+
/** 步骤数明确时把轨道切成等长的小段,`value` 是已完成的段数 */
|
|
12
|
+
segments?: number;
|
|
13
|
+
/** 4px 细条或 8px 标准条 */
|
|
14
|
+
size?: ProgressSize;
|
|
15
|
+
/** 在右侧显示数值:百分比,分段时是 `3 / 6` */
|
|
16
|
+
showValue?: boolean;
|
|
17
|
+
/** 自定义数值的写法 */
|
|
18
|
+
formatValue?: (value: number, max: number) => ReactNode;
|
|
19
|
+
/** 入场时从左侧充填一次 */
|
|
20
|
+
animate?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const trackSize: Record<ProgressSize, string> = {
|
|
24
|
+
sm: "h-1",
|
|
25
|
+
md: "h-2",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* 填充是纯色,不描边:亮色界面上一圈墨色的框显得重。
|
|
30
|
+
* 亮色下它对浅灰轨道只有 1.3:1,确切数值靠 showValue 或旁边的文字给出。
|
|
31
|
+
*/
|
|
32
|
+
const fillClass = "bg-action";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 进度:灰轨 + 黄填充。表示"完成了多少";表示"数量有多少"用图表,那是蓝色的事。
|
|
36
|
+
*
|
|
37
|
+
* `className` 给外层,其余属性(`aria-label` 等)给带 `role="progressbar"` 的轨道。
|
|
38
|
+
*/
|
|
39
|
+
export function Progress({
|
|
40
|
+
value,
|
|
41
|
+
max,
|
|
42
|
+
segments,
|
|
43
|
+
size = "md",
|
|
44
|
+
showValue = false,
|
|
45
|
+
formatValue,
|
|
46
|
+
animate = false,
|
|
47
|
+
className,
|
|
48
|
+
style,
|
|
49
|
+
...props
|
|
50
|
+
}: ProgressProps) {
|
|
51
|
+
const segmented = segments !== undefined && segments > 0;
|
|
52
|
+
const total = max ?? (segmented ? segments : 100);
|
|
53
|
+
const indeterminate = value === undefined;
|
|
54
|
+
const current = indeterminate ? 0 : Math.min(Math.max(value, 0), total);
|
|
55
|
+
const ratio = total > 0 ? current / total : 0;
|
|
56
|
+
|
|
57
|
+
let bar: ReactNode;
|
|
58
|
+
if (segmented) {
|
|
59
|
+
const done = Math.round(ratio * segments);
|
|
60
|
+
bar = (
|
|
61
|
+
<span className="flex h-full gap-1">
|
|
62
|
+
{Array.from({ length: segments }, (_, index) => (
|
|
63
|
+
<span
|
|
64
|
+
key={index}
|
|
65
|
+
data-done={index < done ? "" : undefined}
|
|
66
|
+
className={cn("flex-1", index < done ? fillClass : "bg-line")}
|
|
67
|
+
/>
|
|
68
|
+
))}
|
|
69
|
+
</span>
|
|
70
|
+
);
|
|
71
|
+
} else if (indeterminate) {
|
|
72
|
+
bar = (
|
|
73
|
+
<span
|
|
74
|
+
className={cn(
|
|
75
|
+
"absolute inset-y-0 left-0 w-2/5 animate-indeterminate",
|
|
76
|
+
// 减少动态效果时停在正中,不会被看成"进度 40%"
|
|
77
|
+
"motion-reduce:translate-x-3/4",
|
|
78
|
+
fillClass,
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
} else if (ratio > 0) {
|
|
83
|
+
bar = (
|
|
84
|
+
<span
|
|
85
|
+
className={cn(
|
|
86
|
+
"absolute inset-y-0 left-0 transition-[width] duration-(--duration-base) ease-standard",
|
|
87
|
+
animate && "origin-left animate-fill",
|
|
88
|
+
fillClass,
|
|
89
|
+
)}
|
|
90
|
+
style={{ width: `${ratio * 100}%` }}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let text: ReactNode = null;
|
|
96
|
+
if (showValue && !indeterminate) {
|
|
97
|
+
if (formatValue) text = formatValue(current, total);
|
|
98
|
+
else if (segmented) text = `${current} / ${total}`;
|
|
99
|
+
else text = `${Math.round(ratio * 100)}%`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
className={cn("flex items-center gap-3 text-ink", className)}
|
|
105
|
+
style={style}
|
|
106
|
+
data-size={size}
|
|
107
|
+
>
|
|
108
|
+
<div
|
|
109
|
+
role="progressbar"
|
|
110
|
+
aria-valuemin={0}
|
|
111
|
+
aria-valuemax={total}
|
|
112
|
+
aria-valuenow={indeterminate ? undefined : current}
|
|
113
|
+
aria-valuetext={
|
|
114
|
+
segmented && !indeterminate ? `${current} / ${total}` : undefined
|
|
115
|
+
}
|
|
116
|
+
{...props}
|
|
117
|
+
className={cn(
|
|
118
|
+
"relative min-w-0 flex-1",
|
|
119
|
+
trackSize[size],
|
|
120
|
+
!segmented && "bg-line",
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
{bar}
|
|
124
|
+
</div>
|
|
125
|
+
{text !== null && (
|
|
126
|
+
// 留出"100%"的宽度:几条并排时数值位数不同,轨道也能对齐
|
|
127
|
+
<span className="min-w-[4ch] shrink-0 text-right font-tech text-sm leading-none font-bold tabular-nums">
|
|
128
|
+
{text}
|
|
129
|
+
</span>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type ProgressRingProps = Omit<ComponentProps<"div">, "children"> & {
|
|
5
|
+
/** 当前值。不传就是"不确定进度":一段弧匀速旋转 */
|
|
6
|
+
value?: number;
|
|
7
|
+
/** 最大值,默认 100 */
|
|
8
|
+
max?: number;
|
|
9
|
+
/** 直径,像素,默认 48 */
|
|
10
|
+
size?: number;
|
|
11
|
+
/** 线宽,像素,缺省取直径的 9% */
|
|
12
|
+
thickness?: number;
|
|
13
|
+
/** 在中心显示百分比的数值,不带 `%` */
|
|
14
|
+
showValue?: boolean;
|
|
15
|
+
/** 中心的内容:图标或头像。传了就不显示数值 */
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 进度环:细圆环轨道 + 从 12 点方向顺时针的黄弧。用在紧凑的位置,或者围住一个头像、图标。
|
|
21
|
+
* 取色与 `Progress` 相同;需要名称时传 `aria-label`。
|
|
22
|
+
*/
|
|
23
|
+
export function ProgressRing({
|
|
24
|
+
value,
|
|
25
|
+
max = 100,
|
|
26
|
+
size = 48,
|
|
27
|
+
thickness,
|
|
28
|
+
showValue = false,
|
|
29
|
+
className,
|
|
30
|
+
style,
|
|
31
|
+
children,
|
|
32
|
+
...props
|
|
33
|
+
}: ProgressRingProps) {
|
|
34
|
+
const stroke = thickness ?? Math.max(2, Math.round(size * 0.09));
|
|
35
|
+
const radius = (size - stroke) / 2;
|
|
36
|
+
const circumference = 2 * Math.PI * radius;
|
|
37
|
+
|
|
38
|
+
const indeterminate = value === undefined;
|
|
39
|
+
const current = indeterminate ? 0 : Math.min(Math.max(value, 0), max);
|
|
40
|
+
const ratio = max > 0 ? current / max : 0;
|
|
41
|
+
|
|
42
|
+
const circle = {
|
|
43
|
+
cx: size / 2,
|
|
44
|
+
cy: size / 2,
|
|
45
|
+
r: radius,
|
|
46
|
+
fill: "none",
|
|
47
|
+
strokeWidth: stroke,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let arc: ReactNode = null;
|
|
51
|
+
if (indeterminate) {
|
|
52
|
+
arc = (
|
|
53
|
+
<>
|
|
54
|
+
<circle
|
|
55
|
+
{...circle}
|
|
56
|
+
className="origin-center animate-spin stroke-action motion-reduce:hidden"
|
|
57
|
+
strokeDasharray={`${circumference / 4} ${circumference}`}
|
|
58
|
+
/>
|
|
59
|
+
{/* 减少动态效果时换成一圈虚线:静止的一段弧会被看成"进度 25%" */}
|
|
60
|
+
<circle
|
|
61
|
+
{...circle}
|
|
62
|
+
className="hidden stroke-action motion-reduce:block"
|
|
63
|
+
strokeDasharray={`${stroke} ${stroke * 2}`}
|
|
64
|
+
/>
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
} else if (ratio > 0) {
|
|
68
|
+
arc = (
|
|
69
|
+
<circle
|
|
70
|
+
{...circle}
|
|
71
|
+
className="stroke-action transition-[stroke-dashoffset] duration-(--duration-base) ease-standard"
|
|
72
|
+
strokeDasharray={circumference}
|
|
73
|
+
strokeDashoffset={circumference * (1 - ratio)}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let center: ReactNode = null;
|
|
79
|
+
if (children !== undefined) {
|
|
80
|
+
center = (
|
|
81
|
+
<span className="relative flex items-center justify-center">
|
|
82
|
+
{children}
|
|
83
|
+
</span>
|
|
84
|
+
);
|
|
85
|
+
} else if (showValue && !indeterminate) {
|
|
86
|
+
center = (
|
|
87
|
+
<span
|
|
88
|
+
className="relative font-tech leading-none font-bold tabular-nums"
|
|
89
|
+
style={{ fontSize: Math.round(size * 0.3) }}
|
|
90
|
+
>
|
|
91
|
+
{Math.round(ratio * 100)}
|
|
92
|
+
</span>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
role="progressbar"
|
|
99
|
+
aria-valuemin={0}
|
|
100
|
+
aria-valuemax={max}
|
|
101
|
+
aria-valuenow={indeterminate ? undefined : current}
|
|
102
|
+
{...props}
|
|
103
|
+
className={cn(
|
|
104
|
+
"relative inline-flex shrink-0 items-center justify-center text-ink",
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
107
|
+
style={{ width: size, height: size, ...style }}
|
|
108
|
+
>
|
|
109
|
+
{/* 整张图逆时针转 90°,弧的起点就落在 12 点方向 */}
|
|
110
|
+
<svg
|
|
111
|
+
width={size}
|
|
112
|
+
height={size}
|
|
113
|
+
viewBox={`0 0 ${size} ${size}`}
|
|
114
|
+
aria-hidden="true"
|
|
115
|
+
className="absolute inset-0 -rotate-90"
|
|
116
|
+
>
|
|
117
|
+
<circle {...circle} className="stroke-line" />
|
|
118
|
+
{arc}
|
|
119
|
+
</svg>
|
|
120
|
+
{center}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|