@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,97 @@
|
|
|
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 { popoverDescription, popoverPanel, popoverTitle } from "./panel-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 {...props} className={cn(popoverPanel, className)}>
|
|
67
|
+
{(title || description) && (
|
|
68
|
+
<div className="flex flex-col gap-1">
|
|
69
|
+
{title && (
|
|
70
|
+
<BasePopover.Title className={popoverTitle}>
|
|
71
|
+
{title}
|
|
72
|
+
</BasePopover.Title>
|
|
73
|
+
)}
|
|
74
|
+
{description && (
|
|
75
|
+
<BasePopover.Description className={popoverDescription}>
|
|
76
|
+
{description}
|
|
77
|
+
</BasePopover.Description>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
{children}
|
|
82
|
+
</BasePopover.Popup>
|
|
83
|
+
</BasePopover.Positioner>
|
|
84
|
+
</BasePopover.Portal>
|
|
85
|
+
</BasePopover.Root>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type PopoverCloseProps = {
|
|
90
|
+
/** 点了就关掉面板的按钮,比如"完成" */
|
|
91
|
+
children: ReactElement<Record<string, unknown>>;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** 包住面板里"点了就关"的按钮。 */
|
|
95
|
+
export function PopoverClose({ children }: PopoverCloseProps) {
|
|
96
|
+
return <BasePopover.Close render={children} />;
|
|
97
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { menuPanel } from "../dropdown-menu/menu-style";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 气泡卡片与悬浮卡共用的面板:下拉菜单的那一块,只把上下 4px 的内边距换成四周 16px。
|
|
5
|
+
* 宽度跟着内容走,最宽 320px,也不超过视口里剩下的地方。
|
|
6
|
+
* 要经过 `cn()`:后面的 `p-4` 要盖掉菜单面板自带的上下内边距
|
|
7
|
+
*/
|
|
8
|
+
export const popoverPanel = [
|
|
9
|
+
menuPanel,
|
|
10
|
+
"flex w-max max-w-[min(20rem,var(--available-width))] flex-col gap-3 p-4 text-sm wrap-anywhere",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/** 面板里的标题和它下面的一行说明 */
|
|
14
|
+
export const popoverTitle = "text-base font-bold";
|
|
15
|
+
export const popoverDescription = "text-ink-secondary";
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useId,
|
|
5
|
+
useMemo,
|
|
6
|
+
type ChangeEvent,
|
|
7
|
+
type ComponentProps,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { useControllableState } from "../../hooks/useControllableState";
|
|
11
|
+
import { cn } from "../../lib/cn";
|
|
12
|
+
import { focusRing } from "../../lib/focus-ring";
|
|
13
|
+
import {
|
|
14
|
+
choiceBox,
|
|
15
|
+
choiceControl,
|
|
16
|
+
choiceFrame,
|
|
17
|
+
choiceLabel,
|
|
18
|
+
choiceMark,
|
|
19
|
+
} from "../checkbox/choice-style";
|
|
20
|
+
import { useFieldContext } from "../field/Field";
|
|
21
|
+
|
|
22
|
+
type RadioGroupContextValue = {
|
|
23
|
+
name: string;
|
|
24
|
+
value: string | undefined;
|
|
25
|
+
select: (value: string) => void;
|
|
26
|
+
disabled: boolean;
|
|
27
|
+
required: boolean;
|
|
28
|
+
invalid: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const RadioGroupContext = createContext<RadioGroupContextValue | null>(null);
|
|
32
|
+
|
|
33
|
+
export type RadioGroupOrientation = "vertical" | "horizontal";
|
|
34
|
+
|
|
35
|
+
export type RadioGroupProps = Omit<
|
|
36
|
+
ComponentProps<"div">,
|
|
37
|
+
"onChange" | "defaultValue"
|
|
38
|
+
> & {
|
|
39
|
+
/** 受控的当前值 */
|
|
40
|
+
value?: string;
|
|
41
|
+
/** 非受控时的初始值 */
|
|
42
|
+
defaultValue?: string;
|
|
43
|
+
onValueChange?: (value: string) => void;
|
|
44
|
+
/** 表单提交用的字段名,缺省自动生成 */
|
|
45
|
+
name?: string;
|
|
46
|
+
disabled?: boolean;
|
|
47
|
+
required?: boolean;
|
|
48
|
+
/** 竖排(默认)或横排 */
|
|
49
|
+
orientation?: RadioGroupOrientation;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 单选组。方向键在组内移动并选中,这是原生单选的行为,组件不另外处理。
|
|
54
|
+
* 标签用 `<Field group label="…">` 包住它,或者自己传 `aria-label`。
|
|
55
|
+
*/
|
|
56
|
+
export function RadioGroup({
|
|
57
|
+
value,
|
|
58
|
+
defaultValue,
|
|
59
|
+
onValueChange,
|
|
60
|
+
name,
|
|
61
|
+
disabled: disabledProp,
|
|
62
|
+
required: requiredProp,
|
|
63
|
+
orientation = "vertical",
|
|
64
|
+
className,
|
|
65
|
+
...props
|
|
66
|
+
}: RadioGroupProps) {
|
|
67
|
+
const field = useFieldContext();
|
|
68
|
+
const autoName = useId();
|
|
69
|
+
const [current, setCurrent] = useControllableState<string | undefined>({
|
|
70
|
+
value,
|
|
71
|
+
defaultValue,
|
|
72
|
+
onChange: (next) => {
|
|
73
|
+
if (next !== undefined) onValueChange?.(next);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const disabled = disabledProp ?? field?.disabled ?? false;
|
|
78
|
+
const required = requiredProp ?? field?.required ?? false;
|
|
79
|
+
const invalid = field?.invalid ?? false;
|
|
80
|
+
const groupName = name ?? autoName;
|
|
81
|
+
|
|
82
|
+
const context = useMemo<RadioGroupContextValue>(
|
|
83
|
+
() => ({
|
|
84
|
+
name: groupName,
|
|
85
|
+
value: current,
|
|
86
|
+
select: setCurrent,
|
|
87
|
+
disabled,
|
|
88
|
+
required,
|
|
89
|
+
invalid,
|
|
90
|
+
}),
|
|
91
|
+
[groupName, current, setCurrent, disabled, required, invalid],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<RadioGroupContext value={context}>
|
|
96
|
+
<div
|
|
97
|
+
role="radiogroup"
|
|
98
|
+
// 字段是 <fieldset> 时名称由 <legend> 给;否则指向字段的标签
|
|
99
|
+
aria-labelledby={field && !field.group ? field.labelId : undefined}
|
|
100
|
+
aria-describedby={field?.describedBy}
|
|
101
|
+
aria-invalid={invalid || undefined}
|
|
102
|
+
aria-required={required || undefined}
|
|
103
|
+
aria-disabled={disabled || undefined}
|
|
104
|
+
{...props}
|
|
105
|
+
data-orientation={orientation}
|
|
106
|
+
className={cn(
|
|
107
|
+
"flex",
|
|
108
|
+
orientation === "vertical" ? "flex-col" : "flex-wrap gap-x-6",
|
|
109
|
+
className,
|
|
110
|
+
)}
|
|
111
|
+
/>
|
|
112
|
+
</RadioGroupContext>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type RadioProps = Omit<
|
|
117
|
+
ComponentProps<"input">,
|
|
118
|
+
"type" | "size" | "value"
|
|
119
|
+
> & {
|
|
120
|
+
value: string;
|
|
121
|
+
/** 右侧的文字标签。不传时必须自己给 `aria-label` */
|
|
122
|
+
children?: ReactNode;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 单选框,放在 `RadioGroup` 里。`className` 给整行,其余属性与 `ref` 给 `<input>`。
|
|
127
|
+
* 脱离 `RadioGroup` 时就是一个原生单选,自己管 `name` 与 `checked`。
|
|
128
|
+
*/
|
|
129
|
+
export function Radio({
|
|
130
|
+
value,
|
|
131
|
+
disabled,
|
|
132
|
+
onChange,
|
|
133
|
+
className,
|
|
134
|
+
children,
|
|
135
|
+
...props
|
|
136
|
+
}: RadioProps) {
|
|
137
|
+
const group = useContext(RadioGroupContext);
|
|
138
|
+
|
|
139
|
+
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
140
|
+
onChange?.(event);
|
|
141
|
+
if (event.target.checked) group?.select(value);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const groupProps = group
|
|
145
|
+
? {
|
|
146
|
+
name: group.name,
|
|
147
|
+
checked: group.value === value,
|
|
148
|
+
required: group.required,
|
|
149
|
+
"aria-invalid": group.invalid || undefined,
|
|
150
|
+
}
|
|
151
|
+
: {};
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<label className={cn(choiceLabel, className)}>
|
|
155
|
+
<span className={choiceControl}>
|
|
156
|
+
<span className={choiceFrame}>
|
|
157
|
+
<input
|
|
158
|
+
{...props}
|
|
159
|
+
{...groupProps}
|
|
160
|
+
type="radio"
|
|
161
|
+
value={value}
|
|
162
|
+
disabled={disabled ?? group?.disabled}
|
|
163
|
+
onChange={handleChange}
|
|
164
|
+
className={cn(
|
|
165
|
+
choiceBox,
|
|
166
|
+
"rounded-full choice-diamond:rounded-none",
|
|
167
|
+
focusRing,
|
|
168
|
+
)}
|
|
169
|
+
/>
|
|
170
|
+
<span
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
className={cn(
|
|
173
|
+
choiceMark,
|
|
174
|
+
"size-2 rounded-full bg-accent-ink-inverse peer-checked:opacity-100 peer-disabled:bg-ink-disabled choice-diamond:hidden",
|
|
175
|
+
)}
|
|
176
|
+
/>
|
|
177
|
+
</span>
|
|
178
|
+
</span>
|
|
179
|
+
{children !== undefined && <span className="min-w-0">{children}</span>}
|
|
180
|
+
</label>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type RecIndicatorProps = Omit<ComponentProps<"span">, "children"> & {
|
|
5
|
+
/** 方括号里的字,默认 `REC`;直播、实时数据可以写 `LIVE` */
|
|
6
|
+
label?: ReactNode;
|
|
7
|
+
/** 红点硬切明灭,默认开。关掉后红点常亮 */
|
|
8
|
+
blink?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 录制指示:一个红点 + `[ REC ]`。
|
|
13
|
+
* 只在确有"进行中"的语义时用(正在录制、直播中、实时数据),不要当氛围装饰——
|
|
14
|
+
* 红色在这套语言里是危险与紧急。
|
|
15
|
+
*/
|
|
16
|
+
export function RecIndicator({
|
|
17
|
+
label = "REC",
|
|
18
|
+
blink = true,
|
|
19
|
+
className,
|
|
20
|
+
...props
|
|
21
|
+
}: RecIndicatorProps) {
|
|
22
|
+
return (
|
|
23
|
+
<span
|
|
24
|
+
{...props}
|
|
25
|
+
className={cn(
|
|
26
|
+
"inline-flex items-center gap-1 font-tech text-xs leading-none font-medium whitespace-nowrap text-ink uppercase",
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
{/* 有意不随主题变:角色色里的"危险"红,作填充用 */}
|
|
31
|
+
<span
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
className={cn(
|
|
34
|
+
"mr-0.5 size-2 shrink-0 rounded-full bg-alert",
|
|
35
|
+
blink && "animate-blink",
|
|
36
|
+
)}
|
|
37
|
+
/>
|
|
38
|
+
<span aria-hidden="true" className="text-ink-tertiary">
|
|
39
|
+
[
|
|
40
|
+
</span>
|
|
41
|
+
<span>{label}</span>
|
|
42
|
+
<span aria-hidden="true" className="text-ink-tertiary">
|
|
43
|
+
]
|
|
44
|
+
</span>
|
|
45
|
+
</span>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RecIndicator, type RecIndicatorProps } from "./RecIndicator";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import { decor } from "../../lib/decor";
|
|
4
|
+
|
|
5
|
+
export type RegistrationStripOrientation = "horizontal" | "vertical";
|
|
6
|
+
|
|
7
|
+
export type RegistrationStripProps = Omit<ComponentProps<"div">, "children"> & {
|
|
8
|
+
/** 横条高 3px、每段 64px;竖条是三段 6 × 14px 叠放 */
|
|
9
|
+
orientation?: RegistrationStripOrientation;
|
|
10
|
+
/** 横条后面接一段灰线、撑满容器:用作名称下方的分隔线 */
|
|
11
|
+
rule?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// 有意不随主题变:印刷校准用的三原色,只做装饰,不要用在别处
|
|
15
|
+
const segments = ["bg-reg-magenta", "bg-reg-cyan", "bg-reg-yellow"] as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 注册色条:品红、青、黄三段的微条,印刷品语气的点缀。
|
|
19
|
+
* 纯装饰,不承担任何数据或状态含义;一个版块最多一处,离图表远一点。
|
|
20
|
+
*/
|
|
21
|
+
export function RegistrationStrip({
|
|
22
|
+
orientation = "horizontal",
|
|
23
|
+
rule = false,
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: RegistrationStripProps) {
|
|
27
|
+
const vertical = orientation === "vertical";
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
{...props}
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
data-orientation={orientation}
|
|
34
|
+
className={cn(
|
|
35
|
+
decor,
|
|
36
|
+
vertical
|
|
37
|
+
? "inline-flex flex-col"
|
|
38
|
+
: ["flex h-[3px] max-w-full", rule && "w-full"],
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
{segments.map((color) => (
|
|
43
|
+
<span
|
|
44
|
+
key={color}
|
|
45
|
+
className={cn(color, vertical ? "h-3.5 w-1.5" : "w-16 min-w-0")}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
{rule && !vertical && <span className="min-w-0 flex-1 bg-line" />}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|