@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,182 @@
1
+ import type { ComponentProps, MouseEvent, ReactNode } from "react";
2
+ import { ArrowLeft } from "../../icons/ArrowLeft";
3
+ import { cn } from "../../lib/cn";
4
+ import { focusRing } from "../../lib/focus-ring";
5
+ import { LinkElement, type LinkRender } from "../link-element/LinkElement";
6
+
7
+ export type PageHeaderProps = Omit<ComponentProps<"header">, "title"> & {
8
+ /** 页面的标题 */
9
+ title: ReactNode;
10
+ /** 标题的层级,默认 1(`<h1>`)。页头用在抽屉、分栏里时改小 */
11
+ level?: 1 | 2 | 3;
12
+ /** 标题上面的微文字行,如 `// ARCHIVE 共 128 条` */
13
+ meta?: ReactNode;
14
+ /** 标题下面的一段说明 */
15
+ description?: ReactNode;
16
+ /** 最上面的层级路径:放一个 `Breadcrumb` */
17
+ breadcrumb?: ReactNode;
18
+ /** 标题左边的返回:放一个 `PageHeaderBack` */
19
+ back?: ReactNode;
20
+ /** 右边的行动区:几个按钮。放不下时折到标题下面 */
21
+ actions?: ReactNode;
22
+ /** 底下加一条分隔线 */
23
+ divider?: boolean;
24
+ /**
25
+ * 吸在滚动容器的顶上,带实底和底下那条线。
26
+ * 上面还有吸顶的顶栏时用 `className` 改 `top`
27
+ */
28
+ sticky?: boolean;
29
+ /** 标题下面的一行:页签、几个统计块 */
30
+ children?: ReactNode;
31
+ };
32
+
33
+ /**
34
+ * 页头:这是哪一页、怎么回去、在这一页能做什么。一页一个。
35
+ * 按自身的宽度响应:行动区放不下时折到标题下面。
36
+ */
37
+ export function PageHeader({
38
+ title,
39
+ level = 1,
40
+ meta,
41
+ description,
42
+ breadcrumb,
43
+ back,
44
+ actions,
45
+ divider = false,
46
+ sticky = false,
47
+ className,
48
+ children,
49
+ ...props
50
+ }: PageHeaderProps) {
51
+ const Heading = `h${level}` as const;
52
+
53
+ return (
54
+ <header
55
+ {...props}
56
+ data-sticky={sticky ? "" : undefined}
57
+ className={cn(
58
+ "@container flex flex-col gap-4 text-ink",
59
+ divider && !sticky && "border-b border-line pb-6",
60
+ sticky &&
61
+ "sticky top-0 z-(--z-float) border-b border-line bg-surface py-4",
62
+ className,
63
+ )}
64
+ >
65
+ {breadcrumb}
66
+ <div className="flex flex-col gap-1">
67
+ {meta && <p className="font-tech text-xs text-ink-secondary">{meta}</p>}
68
+ <div className="flex flex-wrap items-start justify-between gap-x-6 gap-y-4">
69
+ {/* 标题这一组至少留 16rem:行动区挤不进来就整块折下去,而不是把标题挤成一个字一行 */}
70
+ <div className="flex min-w-0 flex-1 basis-64 items-start gap-4">
71
+ {/*
72
+ * 标题的一行是 40px 高(宽了是 48px)。返回和行动区各放在同样高的一格里居中,
73
+ * 所以它们对的是标题的第一行——标题折成几行、下面有没有说明都一样
74
+ */}
75
+ {back && (
76
+ <div className="flex h-10 shrink-0 items-center @md:h-12">
77
+ {back}
78
+ </div>
79
+ )}
80
+ <div className="flex min-w-0 flex-col gap-1">
81
+ <Heading className="min-w-0 text-2xl leading-10 font-bold wrap-anywhere @md:text-3xl @md:leading-12">
82
+ {title}
83
+ </Heading>
84
+ {description && (
85
+ <p className="max-w-prose text-sm text-ink-secondary">
86
+ {description}
87
+ </p>
88
+ )}
89
+ </div>
90
+ </div>
91
+ {actions && (
92
+ <div
93
+ data-actions=""
94
+ className="flex min-h-10 flex-wrap items-center gap-2 @md:min-h-12"
95
+ >
96
+ {actions}
97
+ </div>
98
+ )}
99
+ </div>
100
+ </div>
101
+ {children}
102
+ </header>
103
+ );
104
+ }
105
+
106
+ type BackOwnProps = {
107
+ /** 默认"返回"。建议写成"返回某某" */
108
+ "aria-label"?: string;
109
+ /** 传了就渲染成链接 */
110
+ href?: string;
111
+ target?: string;
112
+ rel?: string;
113
+ /** 用这个元素代替 `<a>`(路由库的链接组件)。传了就按链接处理 */
114
+ render?: LinkRender;
115
+ onClick?: (event: MouseEvent<HTMLElement>) => void;
116
+ disabled?: boolean;
117
+ };
118
+
119
+ export type PageHeaderBackProps = BackOwnProps &
120
+ Omit<ComponentProps<"button">, keyof BackOwnProps | "type" | "children">;
121
+
122
+ /**
123
+ * 页头里的返回:40px 的深灰斜纹方块加一个左箭头,`back` 按钮收成的图标形态。
124
+ * 放进 `PageHeader` 的 `back`。
125
+ */
126
+ export function PageHeaderBack({
127
+ "aria-label": label = "返回",
128
+ href,
129
+ target,
130
+ rel,
131
+ render,
132
+ onClick,
133
+ disabled = false,
134
+ className,
135
+ ...props
136
+ }: PageHeaderBackProps) {
137
+ const classes = cn(
138
+ "relative inline-flex size-10 shrink-0 items-center justify-center",
139
+ "transition-colors duration-(--duration-fast) ease-standard",
140
+ focusRing,
141
+ disabled
142
+ ? "cursor-not-allowed bg-disabled text-on-disabled"
143
+ : // 有意不随主题翻转:和 back 按钮是同一块深灰
144
+ "hatch hatch-mid bg-control text-white shadow-xs hover:bg-neutral-600 active:bg-control-pressed",
145
+ className,
146
+ );
147
+ const icon = <ArrowLeft size={20} />;
148
+
149
+ if (href !== undefined || render !== undefined) {
150
+ return (
151
+ <LinkElement
152
+ render={render}
153
+ // 禁用的链接去掉 href,保留 link 角色,让读屏仍能读出"不可用"
154
+ href={disabled ? undefined : href}
155
+ role={disabled ? "link" : undefined}
156
+ aria-disabled={disabled || undefined}
157
+ aria-label={label}
158
+ target={target}
159
+ rel={rel}
160
+ onClick={disabled ? (event) => event.preventDefault() : onClick}
161
+ data-page-header-back=""
162
+ className={classes}
163
+ >
164
+ {icon}
165
+ </LinkElement>
166
+ );
167
+ }
168
+
169
+ return (
170
+ <button
171
+ {...props}
172
+ type="button"
173
+ aria-label={label}
174
+ disabled={disabled}
175
+ onClick={onClick}
176
+ data-page-header-back=""
177
+ className={classes}
178
+ >
179
+ {icon}
180
+ </button>
181
+ );
182
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ PageHeader,
3
+ PageHeaderBack,
4
+ type PageHeaderBackProps,
5
+ type PageHeaderProps,
6
+ } from "./PageHeader";
@@ -0,0 +1,181 @@
1
+ import {
2
+ useState,
3
+ type ChangeEvent,
4
+ type ComponentProps,
5
+ type KeyboardEvent,
6
+ } from "react";
7
+ import { useControllableState } from "../../hooks/useControllableState";
8
+ import { ChevronLeft } from "../../icons/ChevronLeft";
9
+ import { ChevronRight } from "../../icons/ChevronRight";
10
+ import { cn } from "../../lib/cn";
11
+ import { focusRing } from "../../lib/focus-ring";
12
+ import { IconButton, type IconButtonSize } from "../icon-button/IconButton";
13
+
14
+ export type PaginationSize = "md" | "lg";
15
+
16
+ export type PaginationProps = Omit<
17
+ ComponentProps<"nav">,
18
+ "children" | "onChange"
19
+ > & {
20
+ /** 受控的当前页,从 1 起 */
21
+ page?: number;
22
+ /** 非受控时的初始页,默认 1 */
23
+ defaultPage?: number;
24
+ onPageChange?: (page: number) => void;
25
+ /** 总页数 */
26
+ pageCount: number;
27
+ /** 页码处换成输入框:输入页码后回车或失焦跳页,Esc 还原 */
28
+ jump?: boolean;
29
+ /** 条高 48 或 56px */
30
+ size?: PaginationSize;
31
+ prevLabel?: string;
32
+ nextLabel?: string;
33
+ /** 跳页输入框的可访问名称 */
34
+ jumpLabel?: string;
35
+ /** 读屏听到的当前位置,默认"第 1 页,共 12 页" */
36
+ pageLabel?: (page: number, pageCount: number) => string;
37
+ };
38
+
39
+ const barSize: Record<PaginationSize, string> = {
40
+ md: "h-12",
41
+ lg: "h-14",
42
+ };
43
+
44
+ /* 方钮比条高小 8px */
45
+ const buttonSize: Record<PaginationSize, IconButtonSize> = {
46
+ md: "md",
47
+ lg: "lg",
48
+ };
49
+
50
+ const defaultPageLabel = (page: number, pageCount: number) =>
51
+ `第 ${page} 页,共 ${pageCount} 页`;
52
+
53
+ /**
54
+ * 分页条:浅灰斜纹条,两端是方钮,中间是 `01 / 12`。
55
+ * 不列一长串数字按钮;要跳页就打开 `jump`,在页码处直接输入。
56
+ */
57
+ export function Pagination({
58
+ page,
59
+ defaultPage = 1,
60
+ onPageChange,
61
+ pageCount,
62
+ jump = false,
63
+ size = "md",
64
+ prevLabel = "上一页",
65
+ nextLabel = "下一页",
66
+ jumpLabel = "跳到第几页",
67
+ pageLabel = defaultPageLabel,
68
+ className,
69
+ ...props
70
+ }: PaginationProps) {
71
+ const total = Math.max(1, Math.floor(pageCount));
72
+ const clamp = (next: number) => Math.min(Math.max(next, 1), total);
73
+
74
+ const [inner, setPage] = useControllableState({
75
+ value: page,
76
+ defaultValue: defaultPage,
77
+ onChange: onPageChange,
78
+ });
79
+ const current = clamp(inner);
80
+
81
+ // 输入到一半的页码:不为 null 时输入框显示它
82
+ const [draft, setDraft] = useState<string | null>(null);
83
+
84
+ // 至少两位,补零;总页数上百时跟着加宽
85
+ const digits = Math.max(2, String(total).length);
86
+ const pad = (value: number) => String(value).padStart(digits, "0");
87
+
88
+ const go = (next: number) => setPage(clamp(next));
89
+
90
+ const commitDraft = () => {
91
+ if (draft === null) return;
92
+ const typed = Number.parseInt(draft, 10);
93
+ setDraft(null);
94
+ if (Number.isFinite(typed)) go(typed);
95
+ };
96
+
97
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
98
+ setDraft(event.target.value.replace(/\D/g, "").slice(0, digits + 1));
99
+ };
100
+
101
+ const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
102
+ if (event.key === "Enter") {
103
+ event.preventDefault();
104
+ commitDraft();
105
+ } else if (event.key === "Escape") {
106
+ setDraft(null);
107
+ }
108
+ };
109
+
110
+ const squareClass = "bg-surface-raised shadow-sm";
111
+
112
+ return (
113
+ <nav
114
+ aria-label="分页"
115
+ {...props}
116
+ data-size={size}
117
+ className={cn(
118
+ "@container hatch flex items-center justify-between gap-2 bg-surface-muted px-1 text-ink",
119
+ barSize[size],
120
+ className,
121
+ )}
122
+ >
123
+ <IconButton
124
+ variant="accent"
125
+ size={buttonSize[size]}
126
+ aria-label={prevLabel}
127
+ disabled={current <= 1}
128
+ onClick={() => go(current - 1)}
129
+ className={squareClass}
130
+ >
131
+ <ChevronLeft />
132
+ </IconButton>
133
+
134
+ {/* 很窄的容器里页码小一档,三位数的页码才放得下 */}
135
+ <div className="flex min-w-0 items-center gap-2 font-display text-base leading-none whitespace-nowrap tabular-nums @2xs:text-lg">
136
+ {/* 看得见的 01 / 12 对读屏隐藏,换成一句完整的话,翻页时播报 */}
137
+ <span className="sr-only" aria-live="polite">
138
+ {pageLabel(current, total)}
139
+ </span>
140
+ {jump ? (
141
+ <input
142
+ type="text"
143
+ inputMode="numeric"
144
+ autoComplete="off"
145
+ aria-label={jumpLabel}
146
+ value={draft ?? pad(current)}
147
+ onChange={handleChange}
148
+ onKeyDown={handleKeyDown}
149
+ onBlur={commitDraft}
150
+ onFocus={(event) => event.target.select()}
151
+ style={{ width: `calc(${digits}ch + 0.75rem)` }}
152
+ className={cn(
153
+ "h-8 border-b-2 border-line-strong bg-surface-raised text-center font-bold text-ink",
154
+ "transition-colors duration-(--duration-fast) ease-standard",
155
+ "hover:not-focus:border-ink-secondary focus:border-ink",
156
+ focusRing,
157
+ )}
158
+ />
159
+ ) : (
160
+ <span aria-hidden="true" className="font-bold">
161
+ {pad(current)}
162
+ </span>
163
+ )}
164
+ <span aria-hidden="true" className="font-medium text-ink-secondary">
165
+ {`/ ${pad(total)}`}
166
+ </span>
167
+ </div>
168
+
169
+ <IconButton
170
+ variant="accent"
171
+ size={buttonSize[size]}
172
+ aria-label={nextLabel}
173
+ disabled={current >= total}
174
+ onClick={() => go(current + 1)}
175
+ className={squareClass}
176
+ >
177
+ <ChevronRight />
178
+ </IconButton>
179
+ </nav>
180
+ );
181
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ Pagination,
3
+ type PaginationProps,
4
+ type PaginationSize,
5
+ } from "./Pagination";
@@ -0,0 +1,114 @@
1
+ import type { ComponentProps, ElementType, ReactNode } from "react";
2
+ import { cn } from "../../lib/cn";
3
+
4
+ export type PanelProps = ComponentProps<"div"> & {
5
+ /** 1px 描边,默认有。关掉后只靠底色差分层 */
6
+ bordered?: boolean;
7
+ };
8
+
9
+ /** 围住一组信息的容器:直角、底色只比页面差一档、没有阴影。不要面板套面板。 */
10
+ export function Panel({ bordered = true, className, ...props }: PanelProps) {
11
+ return (
12
+ <div
13
+ {...props}
14
+ className={cn(
15
+ "bg-surface-raised text-ink",
16
+ bordered && "border border-line",
17
+ className,
18
+ )}
19
+ />
20
+ );
21
+ }
22
+
23
+ export type PanelHeaderVariant = "band" | "line";
24
+
25
+ export type PanelHeaderProps = Omit<ComponentProps<"div">, "title"> & {
26
+ /**
27
+ * - `band` 反转的横带 + 左端短竖条,默认。它是一个 `data-theme="inverse"` 的
28
+ * 局部主题:里面的按钮、标签、焦点环都按这条带子的底色取值;
29
+ * - `line` 只有一条细线和小标题,更轻。同一个界面选一种。
30
+ */
31
+ variant?: PanelHeaderVariant;
32
+ /** 标题右侧的计数或一个小操作 */
33
+ extra?: ReactNode;
34
+ /** 标题层级,默认 3(`<h3>`) */
35
+ level?: 2 | 3 | 4 | 5 | 6;
36
+ };
37
+
38
+ export function PanelHeader({
39
+ variant = "band",
40
+ extra,
41
+ level = 3,
42
+ className,
43
+ children,
44
+ ...props
45
+ }: PanelHeaderProps) {
46
+ const Heading: ElementType = `h${level}`;
47
+
48
+ return (
49
+ <div
50
+ // 标题带和页面相反:整条换成反转主题,下面照常用 surface / ink
51
+ data-theme={variant === "band" ? "inverse" : undefined}
52
+ {...props}
53
+ className={cn(
54
+ "relative flex items-center justify-between gap-3 px-4 text-sm font-medium text-ink",
55
+ variant === "band"
56
+ ? "min-h-8 bg-surface"
57
+ : "min-h-10 border-b border-line",
58
+ className,
59
+ )}
60
+ >
61
+ {variant === "band" && (
62
+ // 亮色页面上是黄条压墨底,暗色页面上是深黄条压近白底
63
+ <span
64
+ aria-hidden="true"
65
+ className="absolute top-1/2 left-0 h-4 w-1 -translate-y-1/2 bg-accent-ink"
66
+ />
67
+ )}
68
+ <Heading className="min-w-0 truncate">{children}</Heading>
69
+ {extra && <div className="flex shrink-0 items-center gap-2">{extra}</div>}
70
+ </div>
71
+ );
72
+ }
73
+
74
+ export type PanelBodyProps = ComponentProps<"div">;
75
+
76
+ export function PanelBody({ className, ...props }: PanelBodyProps) {
77
+ return <div {...props} className={cn("p-4", className)} />;
78
+ }
79
+
80
+ export type PanelRowsProps = ComponentProps<"dl">;
81
+
82
+ /** 属性行的容器。里面只放 `PanelRow`。 */
83
+ export function PanelRows({ className, ...props }: PanelRowsProps) {
84
+ return <dl {...props} className={cn("px-4", className)} />;
85
+ }
86
+
87
+ export type PanelRowProps = Omit<ComponentProps<"div">, "children"> & {
88
+ /** 属性名,左对齐 */
89
+ label: ReactNode;
90
+ /** 属性值,右对齐、等宽、加粗 */
91
+ children: ReactNode;
92
+ };
93
+
94
+ export function PanelRow({
95
+ label,
96
+ children,
97
+ className,
98
+ ...props
99
+ }: PanelRowProps) {
100
+ return (
101
+ <div
102
+ {...props}
103
+ className={cn(
104
+ "flex items-baseline justify-between gap-4 border-b border-line py-2.5 last:border-b-0",
105
+ className,
106
+ )}
107
+ >
108
+ <dt className="text-sm text-ink-secondary">{label}</dt>
109
+ <dd className="text-right font-tech text-sm font-bold text-ink tabular-nums">
110
+ {children}
111
+ </dd>
112
+ </div>
113
+ );
114
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ Panel,
3
+ PanelBody,
4
+ PanelHeader,
5
+ PanelRow,
6
+ PanelRows,
7
+ type PanelBodyProps,
8
+ type PanelHeaderProps,
9
+ type PanelHeaderVariant,
10
+ type PanelProps,
11
+ type PanelRowProps,
12
+ type PanelRowsProps,
13
+ } from "./Panel";
@@ -0,0 +1,92 @@
1
+ import type { ComponentProps } from "react";
2
+ import { TriangleRight } from "../../icons/TriangleRight";
3
+ import { cn } from "../../lib/cn";
4
+ import { focusRing } from "../../lib/focus-ring";
5
+
6
+ export type PlayMarkSize = "sm" | "md" | "lg";
7
+
8
+ /* 24 / 32 / 40px,三角占一半 */
9
+ const boxSize: Record<PlayMarkSize, string> = {
10
+ sm: "size-6 [&_svg]:size-3",
11
+ md: "size-8 [&_svg]:size-4",
12
+ lg: "size-10 [&_svg]:size-5",
13
+ };
14
+
15
+ /*
16
+ * 黄色小方块 + 墨色三角。有意不随主题变:它压在图上。
17
+ * 黄色压在浅色的图上边界不清楚,认它靠的是里面的三角。
18
+ */
19
+ const markBase =
20
+ "inline-flex shrink-0 items-center justify-center rounded-xs bg-action text-on-action";
21
+
22
+ export type PlayMarkProps = Omit<ComponentProps<"span">, "children"> & {
23
+ size?: PlayMarkSize;
24
+ };
25
+
26
+ /**
27
+ * 播放记号:只是一个记号,不能点,对读屏隐藏。
28
+ * 整张卡已经是一个链接时用它(媒体卡的 `video`);要它自己能点用 `PlayButton`。
29
+ */
30
+ export function PlayMark({ size = "md", className, ...props }: PlayMarkProps) {
31
+ return (
32
+ <span
33
+ aria-hidden="true"
34
+ {...props}
35
+ data-size={size}
36
+ className={cn(markBase, boxSize[size], className)}
37
+ >
38
+ <TriangleRight />
39
+ </span>
40
+ );
41
+ }
42
+
43
+ export type PlayButtonProps = Omit<
44
+ ComponentProps<"button">,
45
+ "children" | "aria-label"
46
+ > & {
47
+ /** 默认"播放"。一页有好几个时写明播的是什么 */
48
+ "aria-label"?: string;
49
+ size?: PlayMarkSize;
50
+ };
51
+
52
+ /** 播放钮:和播放记号同一个画法的按钮,给"点了就在原地播放"的封面用。 */
53
+ export function PlayButton({
54
+ "aria-label": ariaLabel = "播放",
55
+ size = "md",
56
+ type = "button",
57
+ disabled = false,
58
+ className,
59
+ ...props
60
+ }: PlayButtonProps) {
61
+ return (
62
+ <button
63
+ {...props}
64
+ type={type}
65
+ disabled={disabled}
66
+ aria-label={ariaLabel}
67
+ data-size={size}
68
+ className={cn(
69
+ "group relative",
70
+ markBase,
71
+ boxSize[size],
72
+ "transition-colors duration-(--duration-fast) ease-standard",
73
+ focusRing,
74
+ // 24px 小于触屏的最小点击区,用伪元素向外补到 40px
75
+ size === "sm" && "after:absolute after:-inset-2 after:content-['']",
76
+ disabled
77
+ ? "cursor-not-allowed bg-disabled text-on-disabled"
78
+ : "active:bg-action-pressed",
79
+ className,
80
+ )}
81
+ >
82
+ {/* 悬停:记号让一步,和按钮的竖条变箭头是同一种做法 */}
83
+ <TriangleRight
84
+ className={cn(
85
+ "transition-[translate] duration-(--duration-base) ease-standard",
86
+ !disabled &&
87
+ "group-hover:translate-x-0.5 group-active:translate-x-0.5",
88
+ )}
89
+ />
90
+ </button>
91
+ );
92
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ PlayButton,
3
+ PlayMark,
4
+ type PlayButtonProps,
5
+ type PlayMarkProps,
6
+ type PlayMarkSize,
7
+ } from "./PlayButton";