@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,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,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,114 @@
|
|
|
1
|
+
import type { ComponentProps, ElementType, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type PanelProps = ComponentProps<"div"> & {
|
|
5
|
+
/** 1px 描边,默认有。关掉后只靠底色差分层 */
|
|
6
|
+
bordered?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** 围住一组信息的容器:直角、底色只比页面差一档、没有阴影。不要面板套面板。 */
|
|
10
|
+
export function Panel({ bordered = true, className, ...props }: PanelProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
{...props}
|
|
14
|
+
className={cn(
|
|
15
|
+
"bg-surface-raised text-ink",
|
|
16
|
+
bordered && "border border-line",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type PanelHeaderVariant = "band" | "line";
|
|
24
|
+
|
|
25
|
+
export type PanelHeaderProps = Omit<ComponentProps<"div">, "title"> & {
|
|
26
|
+
/**
|
|
27
|
+
* - `band` 反转的横带 + 左端短竖条,默认。它是一个 `data-theme="inverse"` 的
|
|
28
|
+
* 局部主题:里面的按钮、标签、焦点环都按这条带子的底色取值;
|
|
29
|
+
* - `line` 只有一条细线和小标题,更轻。同一个界面选一种。
|
|
30
|
+
*/
|
|
31
|
+
variant?: PanelHeaderVariant;
|
|
32
|
+
/** 标题右侧的计数或一个小操作 */
|
|
33
|
+
extra?: ReactNode;
|
|
34
|
+
/** 标题层级,默认 3(`<h3>`) */
|
|
35
|
+
level?: 2 | 3 | 4 | 5 | 6;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function PanelHeader({
|
|
39
|
+
variant = "band",
|
|
40
|
+
extra,
|
|
41
|
+
level = 3,
|
|
42
|
+
className,
|
|
43
|
+
children,
|
|
44
|
+
...props
|
|
45
|
+
}: PanelHeaderProps) {
|
|
46
|
+
const Heading: ElementType = `h${level}`;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
// 标题带和页面相反:整条换成反转主题,下面照常用 surface / ink
|
|
51
|
+
data-theme={variant === "band" ? "inverse" : undefined}
|
|
52
|
+
{...props}
|
|
53
|
+
className={cn(
|
|
54
|
+
"relative flex items-center justify-between gap-3 px-4 text-sm font-medium text-ink",
|
|
55
|
+
variant === "band"
|
|
56
|
+
? "min-h-8 bg-surface"
|
|
57
|
+
: "min-h-10 border-b border-line",
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
{variant === "band" && (
|
|
62
|
+
// 亮色页面上是黄条压墨底,暗色页面上是深黄条压近白底
|
|
63
|
+
<span
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
className="absolute top-1/2 left-0 h-4 w-1 -translate-y-1/2 bg-accent-ink"
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
<Heading className="min-w-0 truncate">{children}</Heading>
|
|
69
|
+
{extra && <div className="flex shrink-0 items-center gap-2">{extra}</div>}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type PanelBodyProps = ComponentProps<"div">;
|
|
75
|
+
|
|
76
|
+
export function PanelBody({ className, ...props }: PanelBodyProps) {
|
|
77
|
+
return <div {...props} className={cn("p-4", className)} />;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type PanelRowsProps = ComponentProps<"dl">;
|
|
81
|
+
|
|
82
|
+
/** 属性行的容器。里面只放 `PanelRow`。 */
|
|
83
|
+
export function PanelRows({ className, ...props }: PanelRowsProps) {
|
|
84
|
+
return <dl {...props} className={cn("px-4", className)} />;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type PanelRowProps = Omit<ComponentProps<"div">, "children"> & {
|
|
88
|
+
/** 属性名,左对齐 */
|
|
89
|
+
label: ReactNode;
|
|
90
|
+
/** 属性值,右对齐、等宽、加粗 */
|
|
91
|
+
children: ReactNode;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export function PanelRow({
|
|
95
|
+
label,
|
|
96
|
+
children,
|
|
97
|
+
className,
|
|
98
|
+
...props
|
|
99
|
+
}: PanelRowProps) {
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
{...props}
|
|
103
|
+
className={cn(
|
|
104
|
+
"flex items-baseline justify-between gap-4 border-b border-line py-2.5 last:border-b-0",
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
<dt className="text-sm text-ink-secondary">{label}</dt>
|
|
109
|
+
<dd className="text-right font-tech text-sm font-bold text-ink tabular-nums">
|
|
110
|
+
{children}
|
|
111
|
+
</dd>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { TriangleRight } from "../../icons/TriangleRight";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { focusRing } from "../../lib/focus-ring";
|
|
5
|
+
|
|
6
|
+
export type PlayMarkSize = "sm" | "md" | "lg";
|
|
7
|
+
|
|
8
|
+
/* 24 / 32 / 40px,三角占一半 */
|
|
9
|
+
const boxSize: Record<PlayMarkSize, string> = {
|
|
10
|
+
sm: "size-6 [&_svg]:size-3",
|
|
11
|
+
md: "size-8 [&_svg]:size-4",
|
|
12
|
+
lg: "size-10 [&_svg]:size-5",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* 黄色小方块 + 墨色三角。有意不随主题变:它压在图上。
|
|
17
|
+
* 黄色压在浅色的图上边界不清楚,认它靠的是里面的三角。
|
|
18
|
+
*/
|
|
19
|
+
const markBase =
|
|
20
|
+
"inline-flex shrink-0 items-center justify-center rounded-xs bg-action text-on-action";
|
|
21
|
+
|
|
22
|
+
export type PlayMarkProps = Omit<ComponentProps<"span">, "children"> & {
|
|
23
|
+
size?: PlayMarkSize;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 播放记号:只是一个记号,不能点,对读屏隐藏。
|
|
28
|
+
* 整张卡已经是一个链接时用它(媒体卡的 `video`);要它自己能点用 `PlayButton`。
|
|
29
|
+
*/
|
|
30
|
+
export function PlayMark({ size = "md", className, ...props }: PlayMarkProps) {
|
|
31
|
+
return (
|
|
32
|
+
<span
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
{...props}
|
|
35
|
+
data-size={size}
|
|
36
|
+
className={cn(markBase, boxSize[size], className)}
|
|
37
|
+
>
|
|
38
|
+
<TriangleRight />
|
|
39
|
+
</span>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type PlayButtonProps = Omit<
|
|
44
|
+
ComponentProps<"button">,
|
|
45
|
+
"children" | "aria-label"
|
|
46
|
+
> & {
|
|
47
|
+
/** 默认"播放"。一页有好几个时写明播的是什么 */
|
|
48
|
+
"aria-label"?: string;
|
|
49
|
+
size?: PlayMarkSize;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** 播放钮:和播放记号同一个画法的按钮,给"点了就在原地播放"的封面用。 */
|
|
53
|
+
export function PlayButton({
|
|
54
|
+
"aria-label": ariaLabel = "播放",
|
|
55
|
+
size = "md",
|
|
56
|
+
type = "button",
|
|
57
|
+
disabled = false,
|
|
58
|
+
className,
|
|
59
|
+
...props
|
|
60
|
+
}: PlayButtonProps) {
|
|
61
|
+
return (
|
|
62
|
+
<button
|
|
63
|
+
{...props}
|
|
64
|
+
type={type}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
aria-label={ariaLabel}
|
|
67
|
+
data-size={size}
|
|
68
|
+
className={cn(
|
|
69
|
+
"group relative",
|
|
70
|
+
markBase,
|
|
71
|
+
boxSize[size],
|
|
72
|
+
"transition-colors duration-(--duration-fast) ease-standard",
|
|
73
|
+
focusRing,
|
|
74
|
+
// 24px 小于触屏的最小点击区,用伪元素向外补到 40px
|
|
75
|
+
size === "sm" && "after:absolute after:-inset-2 after:content-['']",
|
|
76
|
+
disabled
|
|
77
|
+
? "cursor-not-allowed bg-disabled text-on-disabled"
|
|
78
|
+
: "active:bg-action-pressed",
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{/* 悬停:记号让一步,和按钮的竖条变箭头是同一种做法 */}
|
|
83
|
+
<TriangleRight
|
|
84
|
+
className={cn(
|
|
85
|
+
"transition-[translate] duration-(--duration-base) ease-standard",
|
|
86
|
+
!disabled &&
|
|
87
|
+
"group-hover:translate-x-0.5 group-active:translate-x-0.5",
|
|
88
|
+
)}
|
|
89
|
+
/>
|
|
90
|
+
</button>
|
|
91
|
+
);
|
|
92
|
+
}
|