@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.
Files changed (240) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +27 -0
  3. package/README.md +818 -2
  4. package/dist/index.d.ts +3445 -0
  5. package/dist/index.js +8706 -0
  6. package/dist/styles.css +2 -0
  7. package/package.json +83 -3
  8. package/src/components/README.md +179 -0
  9. package/src/components/accordion/Accordion.tsx +168 -0
  10. package/src/components/accordion/index.ts +8 -0
  11. package/src/components/alert/Alert.tsx +106 -0
  12. package/src/components/alert/index.ts +1 -0
  13. package/src/components/avatar/Avatar.tsx +92 -0
  14. package/src/components/avatar/AvatarSwitcher.tsx +261 -0
  15. package/src/components/avatar/index.ts +8 -0
  16. package/src/components/back-to-top/BackToTop.tsx +124 -0
  17. package/src/components/back-to-top/index.ts +5 -0
  18. package/src/components/badge/Badge.tsx +75 -0
  19. package/src/components/badge/index.ts +1 -0
  20. package/src/components/bracket-title/BracketTitle.tsx +40 -0
  21. package/src/components/bracket-title/index.ts +1 -0
  22. package/src/components/breadcrumb/Breadcrumb.tsx +161 -0
  23. package/src/components/breadcrumb/index.ts +6 -0
  24. package/src/components/button/Button.tsx +357 -0
  25. package/src/components/button/ButtonGroup.tsx +55 -0
  26. package/src/components/button/index.ts +13 -0
  27. package/src/components/calendar/Calendar.tsx +443 -0
  28. package/src/components/calendar/index.ts +7 -0
  29. package/src/components/carousel/Carousel.tsx +252 -0
  30. package/src/components/carousel/index.ts +7 -0
  31. package/src/components/carousel/snap-track.ts +74 -0
  32. package/src/components/checkbox/Checkbox.tsx +94 -0
  33. package/src/components/checkbox/choice-style.ts +30 -0
  34. package/src/components/checkbox/index.ts +1 -0
  35. package/src/components/chip/FilterChip.tsx +68 -0
  36. package/src/components/chip/ResourceChip.tsx +63 -0
  37. package/src/components/chip/capsule-style.ts +22 -0
  38. package/src/components/chip/index.ts +6 -0
  39. package/src/components/combobox/Combobox.tsx +450 -0
  40. package/src/components/combobox/index.ts +10 -0
  41. package/src/components/completion-banner/CompletionBanner.tsx +72 -0
  42. package/src/components/completion-banner/index.ts +4 -0
  43. package/src/components/context-menu/ContextMenu.tsx +70 -0
  44. package/src/components/context-menu/index.ts +5 -0
  45. package/src/components/corner-brackets/CornerBrackets.tsx +43 -0
  46. package/src/components/corner-brackets/index.ts +5 -0
  47. package/src/components/countdown/Countdown.tsx +145 -0
  48. package/src/components/countdown/index.ts +6 -0
  49. package/src/components/dash-indicator/DashIndicator.tsx +44 -0
  50. package/src/components/dash-indicator/index.ts +1 -0
  51. package/src/components/data-row/DataRow.tsx +282 -0
  52. package/src/components/data-row/index.ts +8 -0
  53. package/src/components/date-picker/DateField.tsx +159 -0
  54. package/src/components/date-picker/DatePicker.tsx +182 -0
  55. package/src/components/date-picker/DateRangePicker.tsx +203 -0
  56. package/src/components/date-picker/index.ts +7 -0
  57. package/src/components/dialog/Dialog.tsx +203 -0
  58. package/src/components/dialog/index.ts +7 -0
  59. package/src/components/dialog/initial-focus.ts +19 -0
  60. package/src/components/dialog/overlay-style.ts +28 -0
  61. package/src/components/drawer/Drawer.tsx +213 -0
  62. package/src/components/drawer/index.ts +6 -0
  63. package/src/components/dropdown-menu/DropdownMenu.tsx +427 -0
  64. package/src/components/dropdown-menu/MenuCheck.tsx +44 -0
  65. package/src/components/dropdown-menu/index.ts +22 -0
  66. package/src/components/dropdown-menu/menu-style.ts +47 -0
  67. package/src/components/empty-state/EmptyState.tsx +79 -0
  68. package/src/components/empty-state/index.ts +1 -0
  69. package/src/components/field/Field.tsx +203 -0
  70. package/src/components/field/index.ts +6 -0
  71. package/src/components/file-upload/FileItem.tsx +201 -0
  72. package/src/components/file-upload/FileUpload.tsx +425 -0
  73. package/src/components/file-upload/index.ts +9 -0
  74. package/src/components/flyout-bar/FlyoutBar.tsx +158 -0
  75. package/src/components/flyout-bar/index.ts +7 -0
  76. package/src/components/ghost-text/GhostText.tsx +47 -0
  77. package/src/components/ghost-text/index.ts +5 -0
  78. package/src/components/hatch/Hatch.tsx +35 -0
  79. package/src/components/hatch/index.ts +1 -0
  80. package/src/components/hazard-stripe/HazardStripe.tsx +35 -0
  81. package/src/components/hazard-stripe/index.ts +5 -0
  82. package/src/components/hover-card/HoverCard.tsx +101 -0
  83. package/src/components/hover-card/index.ts +6 -0
  84. package/src/components/icon-button/IconButton.tsx +95 -0
  85. package/src/components/icon-button/index.ts +6 -0
  86. package/src/components/image-viewer/ImageViewer.tsx +427 -0
  87. package/src/components/image-viewer/index.ts +7 -0
  88. package/src/components/input/Input.tsx +109 -0
  89. package/src/components/input/control-box.ts +71 -0
  90. package/src/components/input/index.ts +6 -0
  91. package/src/components/input/value-chip.ts +34 -0
  92. package/src/components/item-slot/ItemGrid.tsx +207 -0
  93. package/src/components/item-slot/ItemSlot.tsx +284 -0
  94. package/src/components/item-slot/index.ts +7 -0
  95. package/src/components/kbd/Kbd.tsx +33 -0
  96. package/src/components/kbd/index.ts +1 -0
  97. package/src/components/link-element/LinkElement.tsx +25 -0
  98. package/src/components/list/List.tsx +229 -0
  99. package/src/components/list/band-style.ts +18 -0
  100. package/src/components/list/index.ts +8 -0
  101. package/src/components/loader/Loader.tsx +154 -0
  102. package/src/components/loader/index.ts +1 -0
  103. package/src/components/marquee/Marquee.tsx +96 -0
  104. package/src/components/marquee/index.ts +1 -0
  105. package/src/components/media-card/MediaCard.tsx +139 -0
  106. package/src/components/media-card/index.ts +5 -0
  107. package/src/components/nav-action/NavAction.tsx +142 -0
  108. package/src/components/nav-action/index.ts +5 -0
  109. package/src/components/nav-menu/NavMenu.tsx +288 -0
  110. package/src/components/nav-menu/index.ts +6 -0
  111. package/src/components/navigator/Navigator.tsx +136 -0
  112. package/src/components/navigator/index.ts +5 -0
  113. package/src/components/otp-input/OtpInput.tsx +211 -0
  114. package/src/components/otp-input/index.ts +7 -0
  115. package/src/components/page-header/PageHeader.tsx +182 -0
  116. package/src/components/page-header/index.ts +6 -0
  117. package/src/components/pagination/Pagination.tsx +181 -0
  118. package/src/components/pagination/index.ts +5 -0
  119. package/src/components/panel/Panel.tsx +114 -0
  120. package/src/components/panel/index.ts +13 -0
  121. package/src/components/play-button/PlayButton.tsx +92 -0
  122. package/src/components/play-button/index.ts +7 -0
  123. package/src/components/popover/Popover.tsx +97 -0
  124. package/src/components/popover/index.ts +8 -0
  125. package/src/components/popover/panel-style.ts +15 -0
  126. package/src/components/progress/Progress.tsx +133 -0
  127. package/src/components/progress/ProgressRing.tsx +123 -0
  128. package/src/components/progress/index.ts +2 -0
  129. package/src/components/radio/Radio.tsx +182 -0
  130. package/src/components/radio/index.ts +7 -0
  131. package/src/components/rec-indicator/RecIndicator.tsx +47 -0
  132. package/src/components/rec-indicator/index.ts +1 -0
  133. package/src/components/registration-strip/RegistrationStrip.tsx +51 -0
  134. package/src/components/registration-strip/index.ts +5 -0
  135. package/src/components/schedule/Schedule.tsx +457 -0
  136. package/src/components/schedule/index.ts +9 -0
  137. package/src/components/scroll-area/ScrollArea.tsx +137 -0
  138. package/src/components/scroll-area/index.ts +5 -0
  139. package/src/components/scroll-hint/ScrollHint.tsx +78 -0
  140. package/src/components/scroll-hint/index.ts +5 -0
  141. package/src/components/section-title/SectionTitle.tsx +202 -0
  142. package/src/components/section-title/index.ts +5 -0
  143. package/src/components/segmented-control/SegmentedControl.tsx +220 -0
  144. package/src/components/segmented-control/index.ts +8 -0
  145. package/src/components/select/Select.tsx +390 -0
  146. package/src/components/select/index.ts +15 -0
  147. package/src/components/side-rail/SideRail.tsx +317 -0
  148. package/src/components/side-rail/SideRailSub.tsx +298 -0
  149. package/src/components/side-rail/index.ts +14 -0
  150. package/src/components/skeleton/Skeleton.tsx +70 -0
  151. package/src/components/skeleton/index.ts +1 -0
  152. package/src/components/slider/Slider.tsx +219 -0
  153. package/src/components/slider/index.ts +8 -0
  154. package/src/components/sparkline/Sparkline.tsx +122 -0
  155. package/src/components/sparkline/index.ts +6 -0
  156. package/src/components/spinner/Spinner.tsx +64 -0
  157. package/src/components/spinner/index.ts +1 -0
  158. package/src/components/stat/Stat.tsx +79 -0
  159. package/src/components/stat/index.ts +1 -0
  160. package/src/components/stepper/Stepper.tsx +316 -0
  161. package/src/components/stepper/index.ts +1 -0
  162. package/src/components/steps/Steps.tsx +302 -0
  163. package/src/components/steps/index.ts +10 -0
  164. package/src/components/switch/Switch.tsx +120 -0
  165. package/src/components/switch/index.ts +1 -0
  166. package/src/components/table/Table.tsx +560 -0
  167. package/src/components/table/index.ts +20 -0
  168. package/src/components/tabs/Tabs.tsx +313 -0
  169. package/src/components/tabs/index.ts +12 -0
  170. package/src/components/tag/Tag.tsx +66 -0
  171. package/src/components/tag/TagPair.tsx +34 -0
  172. package/src/components/tag/index.ts +2 -0
  173. package/src/components/tag-input/TagInput.tsx +502 -0
  174. package/src/components/tag-input/index.ts +8 -0
  175. package/src/components/term/Term.tsx +53 -0
  176. package/src/components/term/index.ts +1 -0
  177. package/src/components/textarea/Textarea.tsx +99 -0
  178. package/src/components/textarea/index.ts +1 -0
  179. package/src/components/texture/Texture.tsx +45 -0
  180. package/src/components/texture/index.ts +1 -0
  181. package/src/components/tick-ring/TickRing.tsx +77 -0
  182. package/src/components/tick-ring/index.ts +1 -0
  183. package/src/components/timeline/Timeline.tsx +84 -0
  184. package/src/components/timeline/index.ts +7 -0
  185. package/src/components/toast/Toast.tsx +257 -0
  186. package/src/components/toast/index.ts +10 -0
  187. package/src/components/toc/Toc.tsx +263 -0
  188. package/src/components/toc/index.ts +7 -0
  189. package/src/components/toolbar/Toolbar.tsx +389 -0
  190. package/src/components/toolbar/index.ts +17 -0
  191. package/src/components/tooltip/Tooltip.tsx +175 -0
  192. package/src/components/tooltip/index.ts +8 -0
  193. package/src/components/top-bar/TopBar.tsx +47 -0
  194. package/src/components/top-bar/index.ts +1 -0
  195. package/src/components/viewfinder/Viewfinder.tsx +97 -0
  196. package/src/components/viewfinder/index.ts +6 -0
  197. package/src/hooks/README.md +14 -0
  198. package/src/hooks/index.ts +4 -0
  199. package/src/hooks/useControllableState.ts +30 -0
  200. package/src/hooks/useInView.ts +44 -0
  201. package/src/hooks/useOverflowing.ts +28 -0
  202. package/src/hooks/usePortalScope.ts +68 -0
  203. package/src/hooks/useScrollPosition.ts +57 -0
  204. package/src/hooks/useTheme.ts +102 -0
  205. package/src/icons/ArrowCorner.tsx +7 -0
  206. package/src/icons/ArrowLeft.tsx +6 -0
  207. package/src/icons/ArrowRight.tsx +6 -0
  208. package/src/icons/CalendarIcon.tsx +10 -0
  209. package/src/icons/Check.tsx +4 -0
  210. package/src/icons/ChevronDown.tsx +3 -0
  211. package/src/icons/ChevronLeft.tsx +6 -0
  212. package/src/icons/ChevronRight.tsx +6 -0
  213. package/src/icons/Close.tsx +3 -0
  214. package/src/icons/Crosshair.tsx +7 -0
  215. package/src/icons/Lock.tsx +6 -0
  216. package/src/icons/Menu.tsx +4 -0
  217. package/src/icons/Minus.tsx +4 -0
  218. package/src/icons/Plus.tsx +3 -0
  219. package/src/icons/README.md +31 -0
  220. package/src/icons/StatusDanger.tsx +7 -0
  221. package/src/icons/StatusInfo.tsx +7 -0
  222. package/src/icons/StatusSuccess.tsx +10 -0
  223. package/src/icons/StatusWarning.tsx +7 -0
  224. package/src/icons/TriangleRight.tsx +7 -0
  225. package/src/icons/Upload.tsx +7 -0
  226. package/src/icons/createIcon.tsx +34 -0
  227. package/src/icons/index.ts +21 -0
  228. package/src/index.ts +87 -0
  229. package/src/lib/README.md +17 -0
  230. package/src/lib/cn.ts +34 -0
  231. package/src/lib/date.ts +125 -0
  232. package/src/lib/decor.ts +6 -0
  233. package/src/lib/defined.ts +9 -0
  234. package/src/lib/file.ts +61 -0
  235. package/src/lib/focus-ring.ts +11 -0
  236. package/src/lib/merge-refs.ts +11 -0
  237. package/src/styles/README.md +70 -0
  238. package/src/styles/index.css +25 -0
  239. package/src/styles/theme.css +568 -0
  240. 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,8 @@
1
+ export {
2
+ Popover,
3
+ PopoverClose,
4
+ type PopoverAlign,
5
+ type PopoverCloseProps,
6
+ type PopoverProps,
7
+ type PopoverSide,
8
+ } from "./Popover";
@@ -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,2 @@
1
+ export { Progress, type ProgressProps, type ProgressSize } from "./Progress";
2
+ export { ProgressRing, type ProgressRingProps } from "./ProgressRing";
@@ -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,7 @@
1
+ export {
2
+ Radio,
3
+ RadioGroup,
4
+ type RadioGroupOrientation,
5
+ type RadioGroupProps,
6
+ type RadioProps,
7
+ } from "./Radio";
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ RegistrationStrip,
3
+ type RegistrationStripOrientation,
4
+ type RegistrationStripProps,
5
+ } from "./RegistrationStrip";