@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,154 @@
1
+ import {
2
+ useEffect,
3
+ useRef,
4
+ useState,
5
+ type ComponentProps,
6
+ type KeyboardEvent,
7
+ type ReactNode,
8
+ type TransitionEvent,
9
+ } from "react";
10
+ import { cn } from "../../lib/cn";
11
+
12
+ export type LoaderProps = Omit<ComponentProps<"div">, "children"> & {
13
+ /** 真实进度,0 – 100。不知道就不传:显示不确定进度,不显示数字 */
14
+ value?: number;
15
+ /** 进度下方的一行标语 */
16
+ tagline?: ReactNode;
17
+ /** 置为 `false` 时遮罩向上滑出,结束后卸载。默认 `true` */
18
+ open?: boolean;
19
+ /** 铺满视口并锁住页面(默认);关掉则铺满所在的定位容器 */
20
+ fullscreen?: boolean;
21
+ /** 进度条的可访问名称 */
22
+ label?: string;
23
+ /** 滑出结束、已经卸载之后触发 */
24
+ onExited?: () => void;
25
+ };
26
+
27
+ /** 滑出的时长,与 `--duration-slower` 一致;过渡事件没来时靠它兜底 */
28
+ const EXIT_MS = 600;
29
+
30
+ /**
31
+ * 加载页:近黑底、大号等宽百分比、一行标语、黄色进度条。只用于整页的首次加载。
32
+ * 不伪造进度;内容就绪后立刻把 `open` 置为 `false`,不要为了播完动画而等待。
33
+ */
34
+ export function Loader({
35
+ value,
36
+ tagline,
37
+ open = true,
38
+ fullscreen = true,
39
+ label = "加载中",
40
+ onExited,
41
+ onKeyDown,
42
+ onTransitionEnd,
43
+ className,
44
+ ...props
45
+ }: LoaderProps) {
46
+ const rootRef = useRef<HTMLDivElement>(null);
47
+ const [mounted, setMounted] = useState(open);
48
+ // 重新打开:立刻挂回来
49
+ if (open && !mounted) setMounted(true);
50
+
51
+ const onExitedRef = useRef(onExited);
52
+ useEffect(() => {
53
+ onExitedRef.current = onExited;
54
+ });
55
+
56
+ const leaving = !open && mounted;
57
+
58
+ // 退出:滑出的过渡结束后卸载。没有过渡的环境里事件不会来,由定时器兜底
59
+ useEffect(() => {
60
+ if (!leaving) return;
61
+ const timer = window.setTimeout(() => {
62
+ setMounted(false);
63
+ onExitedRef.current?.();
64
+ }, EXIT_MS + 100);
65
+ return () => window.clearTimeout(timer);
66
+ }, [leaving]);
67
+
68
+ // 整页模式:加载期间锁住页面滚动,把焦点收进来;开始退出时就放开
69
+ useEffect(() => {
70
+ if (!fullscreen || !open) return;
71
+ const page = document.documentElement;
72
+ const previousOverflow = page.style.overflow;
73
+ const previousFocus = document.activeElement;
74
+ page.style.overflow = "hidden";
75
+ rootRef.current?.focus({ preventScroll: true });
76
+ return () => {
77
+ page.style.overflow = previousOverflow;
78
+ if (previousFocus instanceof HTMLElement && previousFocus.isConnected) {
79
+ previousFocus.focus({ preventScroll: true });
80
+ }
81
+ };
82
+ }, [fullscreen, open]);
83
+
84
+ if (!mounted) return null;
85
+
86
+ const handleTransitionEnd = (event: TransitionEvent<HTMLDivElement>) => {
87
+ onTransitionEnd?.(event);
88
+ if (leaving && event.target === event.currentTarget) {
89
+ setMounted(false);
90
+ onExitedRef.current?.();
91
+ }
92
+ };
93
+
94
+ const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
95
+ onKeyDown?.(event);
96
+ // 整页加载时焦点留在加载页上,不让 Tab 走到被盖住的内容里
97
+ if (fullscreen && open && event.key === "Tab") event.preventDefault();
98
+ };
99
+
100
+ const indeterminate = value === undefined;
101
+ const current = indeterminate ? 0 : Math.min(Math.max(value, 0), 100);
102
+
103
+ return (
104
+ <div
105
+ ref={rootRef}
106
+ tabIndex={-1}
107
+ {...props}
108
+ data-state={open ? "open" : "closing"}
109
+ onKeyDown={handleKeyDown}
110
+ onTransitionEnd={handleTransitionEnd}
111
+ className={cn(
112
+ // 有意不随主题变:加载页是固定的近黑底
113
+ "@container flex flex-col justify-end bg-neutral-950 text-neutral-0 outline-none",
114
+ fullscreen ? "fixed inset-0 z-(--z-loader)" : "absolute inset-0",
115
+ "transition-[translate] duration-(--duration-slower) ease-emphasis",
116
+ open ? "translate-y-0" : "pointer-events-none -translate-y-full",
117
+ className,
118
+ )}
119
+ >
120
+ <div className="flex flex-col gap-4 p-6 @md:p-10">
121
+ {!indeterminate && (
122
+ // 数字与 % 两种字号;确切的值由下面的进度条交给读屏
123
+ <p
124
+ aria-hidden="true"
125
+ className="font-tech leading-none font-medium tabular-nums"
126
+ >
127
+ <span className="text-4xl @md:text-5xl">{Math.round(current)}</span>
128
+ <span className="text-2xl font-normal @md:text-4xl">%</span>
129
+ </p>
130
+ )}
131
+ {tagline !== undefined && (
132
+ <p className="text-base @md:text-xl">{tagline}</p>
133
+ )}
134
+ <div
135
+ role="progressbar"
136
+ aria-label={label}
137
+ aria-valuemin={0}
138
+ aria-valuemax={100}
139
+ aria-valuenow={indeterminate ? undefined : current}
140
+ className="relative h-1.5 overflow-hidden rounded-sm bg-neutral-800"
141
+ >
142
+ {indeterminate ? (
143
+ <span className="absolute inset-y-0 left-0 w-2/5 animate-indeterminate bg-action motion-reduce:translate-x-3/4" />
144
+ ) : (
145
+ <span
146
+ className="absolute inset-0 origin-left bg-action transition-transform duration-(--duration-base) ease-standard"
147
+ style={{ transform: `scaleX(${current / 100})` }}
148
+ />
149
+ )}
150
+ </div>
151
+ </div>
152
+ </div>
153
+ );
154
+ }
@@ -0,0 +1 @@
1
+ export { Loader, type LoaderProps } from "./Loader";
@@ -0,0 +1,96 @@
1
+ import { useEffect, useRef, useState, type ComponentProps } from "react";
2
+ import { cn } from "../../lib/cn";
3
+
4
+ export type MarqueeProps = ComponentProps<"div"> & {
5
+ /** 走完一程的秒数,默认 20。内容越长给得越大 */
6
+ duration?: number;
7
+ /** 首尾两份内容之间的间隔,任意 CSS 长度,默认 `2rem` */
8
+ gap?: string;
9
+ /** 鼠标悬停或焦点落到里面时暂停,默认开 */
10
+ pauseOnHover?: boolean;
11
+ /** 由使用方控制的暂停 */
12
+ paused?: boolean;
13
+ /** 只在内容放不下时才动;放得下就是一行普通的文字 */
14
+ overflowOnly?: boolean;
15
+ };
16
+
17
+ /**
18
+ * 跑马灯:内容复制一份首尾相接,匀速走到一半后停顿一下再循环。
19
+ * 用在巨字和过长的单行文字上;系统开启"减少动态效果"时静止在起点。
20
+ * 纯装饰的巨字给它加 `aria-hidden`。
21
+ */
22
+ export function Marquee({
23
+ duration = 20,
24
+ gap = "2rem",
25
+ pauseOnHover = true,
26
+ paused = false,
27
+ overflowOnly = false,
28
+ className,
29
+ children,
30
+ ...props
31
+ }: MarqueeProps) {
32
+ const rootRef = useRef<HTMLDivElement>(null);
33
+ const copyRef = useRef<HTMLDivElement>(null);
34
+ // 量出来之前先当它放得下:先静止、再动起来,比先动一下再停住自然
35
+ const [fits, setFits] = useState(true);
36
+
37
+ useEffect(() => {
38
+ const root = rootRef.current;
39
+ const copy = copyRef.current;
40
+ if (!overflowOnly || !root || !copy) return;
41
+
42
+ const measure = () => {
43
+ setFits(copy.getBoundingClientRect().width <= root.clientWidth);
44
+ };
45
+ measure();
46
+ if (typeof ResizeObserver === "undefined") return;
47
+ const observer = new ResizeObserver(measure);
48
+ observer.observe(root);
49
+ observer.observe(copy);
50
+ return () => observer.disconnect();
51
+ }, [overflowOnly]);
52
+
53
+ const moving = !(overflowOnly && fits);
54
+ // 间隔用外边距:它不算进一份内容自己的宽度,量"放不放得下"时不用再扣掉
55
+ const copyStyle = moving ? { marginRight: gap } : undefined;
56
+
57
+ return (
58
+ <div
59
+ ref={rootRef}
60
+ {...props}
61
+ data-moving={moving ? "" : undefined}
62
+ className={cn("group/marquee flex overflow-hidden", className)}
63
+ >
64
+ <div
65
+ className={cn(
66
+ "flex w-max shrink-0",
67
+ moving && "animate-marquee motion-reduce:animate-none",
68
+ moving &&
69
+ pauseOnHover &&
70
+ "group-hover/marquee:[animation-play-state:paused] group-focus-within/marquee:[animation-play-state:paused]",
71
+ paused && "[animation-play-state:paused]",
72
+ )}
73
+ style={moving ? { animationDuration: `${duration}s` } : undefined}
74
+ >
75
+ <div
76
+ ref={copyRef}
77
+ className="flex shrink-0 items-center"
78
+ style={copyStyle}
79
+ >
80
+ {children}
81
+ </div>
82
+ {moving && (
83
+ // 副本只为首尾相接:对读屏隐藏、不可聚焦,减少动态效果时不显示
84
+ <div
85
+ aria-hidden="true"
86
+ inert
87
+ className="flex shrink-0 items-center motion-reduce:hidden"
88
+ style={copyStyle}
89
+ >
90
+ {children}
91
+ </div>
92
+ )}
93
+ </div>
94
+ </div>
95
+ );
96
+ }
@@ -0,0 +1 @@
1
+ export { Marquee, type MarqueeProps } from "./Marquee";
@@ -0,0 +1,139 @@
1
+ import type { ComponentProps, ReactNode } from "react";
2
+ import { cn } from "../../lib/cn";
3
+ import { focusRingWithin } from "../../lib/focus-ring";
4
+ import { LinkElement, type LinkRender } from "../link-element/LinkElement";
5
+ import { PlayMark } from "../play-button/PlayButton";
6
+
7
+ export type MediaCardRatio = "16/9" | "4/3" | "1/1" | "3/4";
8
+
9
+ export type MediaCardProps = Omit<ComponentProps<"article">, "title"> & {
10
+ /** 图片、视频封面或任意占位图形,铺满媒体区 */
11
+ media: ReactNode;
12
+ /** 媒体区的宽高比 */
13
+ ratio?: MediaCardRatio;
14
+ title: ReactNode;
15
+ /** 微标行里的类目,组件在前面加 `//` */
16
+ category?: ReactNode;
17
+ /** 微标行里的日期 */
18
+ date?: ReactNode;
19
+ /** 类型标签(如 `PV`),放在图片下方的微标行里,不叠在图上 */
20
+ tag?: ReactNode;
21
+ /** 这张卡是一段影像:媒体区左下角多一个播放记号。记号不能点,整卡仍然只有一个链接 */
22
+ video?: boolean;
23
+ /** `video` 时加在标题前、读屏才听得到的那个词,默认"视频" */
24
+ videoLabel?: string;
25
+ /** 传了就整卡可点:标题是链接,点击范围铺满整张卡 */
26
+ href?: string;
27
+ target?: string;
28
+ rel?: string;
29
+ /** 用这个元素代替标题里的 `<a>`(路由库的链接组件)。传了整卡同样可点 */
30
+ render?: LinkRender;
31
+ /** 标题的层级,默认 `h3` */
32
+ level?: 2 | 3 | 4 | 5 | 6;
33
+ };
34
+
35
+ const ratioClass: Record<MediaCardRatio, string> = {
36
+ "16/9": "aspect-video",
37
+ "4/3": "aspect-4/3",
38
+ "1/1": "aspect-square",
39
+ "3/4": "aspect-3/4",
40
+ };
41
+
42
+ /**
43
+ * 媒体卡:图片 + 微标行 + 标题。标题和标签都在图片之外。
44
+ * 整卡只有一个可点击元素,不要在里面再放按钮或链接。
45
+ */
46
+ export function MediaCard({
47
+ media,
48
+ ratio = "16/9",
49
+ title,
50
+ category,
51
+ date,
52
+ tag,
53
+ video = false,
54
+ videoLabel = "视频",
55
+ href,
56
+ target,
57
+ rel,
58
+ render,
59
+ level = 3,
60
+ className,
61
+ ...props
62
+ }: MediaCardProps) {
63
+ const Heading = `h${level}` as const;
64
+ const isLink = href !== undefined || render !== undefined;
65
+ const hasCategory = category !== undefined && category !== null;
66
+ const hasDate = date !== undefined && date !== null;
67
+ const hasMeta = hasCategory || hasDate;
68
+ // 播放记号是画给眼睛看的;读屏在标题前听到同一件事
69
+ const heading = video ? (
70
+ <>
71
+ <span className="sr-only">{`${videoLabel}:`}</span>
72
+ {title}
73
+ </>
74
+ ) : (
75
+ title
76
+ );
77
+
78
+ return (
79
+ <article
80
+ {...props}
81
+ className={cn(
82
+ "group/card relative flex min-w-0 flex-col text-ink",
83
+ isLink && focusRingWithin,
84
+ className,
85
+ )}
86
+ >
87
+ <div
88
+ className={cn(
89
+ "relative overflow-clip rounded-md bg-surface-muted",
90
+ "[&>img]:size-full [&>img]:object-cover [&>svg]:size-full [&>video]:size-full [&>video]:object-cover",
91
+ ratioClass[ratio],
92
+ )}
93
+ >
94
+ {media}
95
+ {video && (
96
+ // 内缩 8px:不贴着圆角,也不挡底边那条悬停线
97
+ <PlayMark className="pointer-events-none absolute bottom-2 left-2" />
98
+ )}
99
+ {isLink && (
100
+ // 悬停:一条底边强调线从左充出。不放大、不上浮
101
+ <span
102
+ aria-hidden="true"
103
+ className="pointer-events-none absolute inset-x-0 bottom-0 h-1 origin-left scale-x-0 bg-action transition-transform duration-(--duration-base) ease-standard group-hover/card:scale-x-100 group-has-focus-visible/card:scale-x-100"
104
+ />
105
+ )}
106
+ </div>
107
+ {(tag !== undefined || hasMeta) && (
108
+ // 行高按类型标签的高度留足:一排卡片里有的带标签、有的不带,标题仍然对齐
109
+ <p className="mt-3 flex min-h-5 flex-wrap items-center gap-x-2 gap-y-1 text-xs text-ink-secondary">
110
+ {tag}
111
+ {hasMeta && (
112
+ <span className="font-tech">
113
+ <span aria-hidden="true">{"// "}</span>
114
+ {category}
115
+ {hasCategory && hasDate && " "}
116
+ {date}
117
+ </span>
118
+ )}
119
+ </p>
120
+ )}
121
+ <Heading className="mt-1.5 text-base font-medium wrap-anywhere">
122
+ {isLink ? (
123
+ <LinkElement
124
+ render={render}
125
+ href={href}
126
+ target={target}
127
+ rel={rel}
128
+ // 焦点环画在整张卡上;点击范围用伪元素铺满整张卡
129
+ className="outline-none after:absolute after:inset-0 after:content-['']"
130
+ >
131
+ {heading}
132
+ </LinkElement>
133
+ ) : (
134
+ heading
135
+ )}
136
+ </Heading>
137
+ </article>
138
+ );
139
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ MediaCard,
3
+ type MediaCardProps,
4
+ type MediaCardRatio,
5
+ } from "./MediaCard";
@@ -0,0 +1,142 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ type ComponentProps,
5
+ type MouseEvent,
6
+ type ReactNode,
7
+ } from "react";
8
+ import { TriangleRight } from "../../icons/TriangleRight";
9
+ import { cn } from "../../lib/cn";
10
+ import { focusRing } from "../../lib/focus-ring";
11
+ import { LinkElement, type LinkRender } from "../link-element/LinkElement";
12
+
13
+ export type NavActionLayout = "inline" | "block" | "stacked";
14
+
15
+ /** 侧轨和全屏菜单用它告诉里面的主行动块该怎么摆。不对外导出 */
16
+ export const NavActionContext = createContext<NavActionLayout | null>(null);
17
+
18
+ type OwnProps = {
19
+ /** 换掉默认的实心三角 */
20
+ icon?: ReactNode;
21
+ /**
22
+ * 摆法。放在侧轨、全屏菜单里时由它们决定,一般不用传:
23
+ * - `inline` 40px 高的横块,顶栏里;
24
+ * - `block` 48px 高的通宽横条,展开的侧轨和全屏菜单的底部;
25
+ * - `stacked` 48px 宽、至少 96px 高的竖块,文字竖排,收起的侧轨里。
26
+ */
27
+ layout?: NavActionLayout;
28
+ /** 传了就渲染成链接 */
29
+ href?: string;
30
+ target?: string;
31
+ rel?: string;
32
+ /** 用这个元素代替 `<a>`(路由库的链接组件)。传了就按链接处理 */
33
+ render?: LinkRender;
34
+ onClick?: (event: MouseEvent<HTMLElement>) => void;
35
+ disabled?: boolean;
36
+ children: ReactNode;
37
+ };
38
+
39
+ export type NavActionProps = OwnProps &
40
+ Omit<ComponentProps<"button">, keyof OwnProps | "type">;
41
+
42
+ const layoutClass: Record<NavActionLayout, string> = {
43
+ inline: "inline-flex h-10 items-center gap-2.5 px-4 text-sm",
44
+ block: "flex h-12 w-full items-center gap-3 px-4 text-base",
45
+ // 竖排的文字长了就往下长:96px 是最小高度
46
+ stacked:
47
+ "flex min-h-24 w-12 flex-col items-center justify-center gap-2 py-3 text-sm",
48
+ };
49
+
50
+ /**
51
+ * 主行动块:整个产品最主要的那一个去处("前往控制台")。
52
+ * 反转的一整块,悬停时变成信号黄。侧轨、顶栏、全屏菜单的底部放的都是它;
53
+ * 一个界面里只放一个。
54
+ */
55
+ export function NavAction({
56
+ icon,
57
+ layout: layoutProp,
58
+ href,
59
+ target,
60
+ rel,
61
+ render,
62
+ onClick,
63
+ disabled = false,
64
+ className,
65
+ children,
66
+ ...props
67
+ }: NavActionProps) {
68
+ const inherited = useContext(NavActionContext);
69
+ const layout = layoutProp ?? inherited ?? "inline";
70
+ const stacked = layout === "stacked";
71
+
72
+ const classes = cn(
73
+ "shrink-0 font-medium whitespace-nowrap select-none",
74
+ "transition-colors duration-(--duration-fast) ease-standard",
75
+ focusRing,
76
+ layoutClass[layout],
77
+ disabled
78
+ ? "cursor-not-allowed bg-disabled text-on-disabled"
79
+ : "bg-surface-inverse text-ink-inverse hover:bg-action hover:text-on-action active:bg-action-pressed active:text-on-action",
80
+ className,
81
+ );
82
+
83
+ const content = (
84
+ <>
85
+ <span
86
+ aria-hidden="true"
87
+ className="flex shrink-0 items-center justify-center [&_svg]:size-3"
88
+ >
89
+ {icon ?? <TriangleRight />}
90
+ </span>
91
+ {/* 图标与文字之间一条半透明的细线,跟着字色走 */}
92
+ <span
93
+ aria-hidden="true"
94
+ className={cn(
95
+ "shrink-0 bg-current opacity-30",
96
+ stacked ? "h-px w-4" : "h-4 w-px",
97
+ )}
98
+ />
99
+ <span
100
+ className={cn(
101
+ stacked
102
+ ? "tracking-label [writing-mode:vertical-rl]"
103
+ : "min-w-0 truncate",
104
+ )}
105
+ >
106
+ {children}
107
+ </span>
108
+ </>
109
+ );
110
+
111
+ if (href !== undefined || render !== undefined) {
112
+ return (
113
+ <LinkElement
114
+ render={render}
115
+ // 禁用的链接去掉 href,保留 link 角色,让读屏仍能读出"不可用"
116
+ href={disabled ? undefined : href}
117
+ role={disabled ? "link" : undefined}
118
+ aria-disabled={disabled || undefined}
119
+ target={target}
120
+ rel={rel}
121
+ onClick={disabled ? (event) => event.preventDefault() : onClick}
122
+ data-layout={layout}
123
+ className={classes}
124
+ >
125
+ {content}
126
+ </LinkElement>
127
+ );
128
+ }
129
+
130
+ return (
131
+ <button
132
+ {...props}
133
+ type="button"
134
+ disabled={disabled}
135
+ onClick={onClick}
136
+ data-layout={layout}
137
+ className={classes}
138
+ >
139
+ {content}
140
+ </button>
141
+ );
142
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ NavAction,
3
+ type NavActionLayout,
4
+ type NavActionProps,
5
+ } from "./NavAction";