@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,457 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useMemo,
|
|
5
|
+
type ComponentProps,
|
|
6
|
+
type CSSProperties,
|
|
7
|
+
type MouseEvent,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { useOverflowing } from "../../hooks/useOverflowing";
|
|
11
|
+
import { cn } from "../../lib/cn";
|
|
12
|
+
import { dateParts, daysBetween, eachDay, toDateObject } from "../../lib/date";
|
|
13
|
+
import { decor } from "../../lib/decor";
|
|
14
|
+
import { focusRingInset } from "../../lib/focus-ring";
|
|
15
|
+
import { LinkElement, type LinkRender } from "../link-element/LinkElement";
|
|
16
|
+
import { Tag } from "../tag/Tag";
|
|
17
|
+
|
|
18
|
+
type ScheduleContextValue = {
|
|
19
|
+
start: string;
|
|
20
|
+
/** 一共几天 */
|
|
21
|
+
days: number;
|
|
22
|
+
/** 读屏听到的起止日期 */
|
|
23
|
+
describe: (start: string, end: string) => string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const ScheduleContext = createContext<ScheduleContextValue | null>(null);
|
|
27
|
+
|
|
28
|
+
function useSchedule(part: string) {
|
|
29
|
+
const context = useContext(ScheduleContext);
|
|
30
|
+
if (!context) throw new Error(`${part} 要放在 Schedule 里面`);
|
|
31
|
+
return context;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ScheduleProps = Omit<ComponentProps<"div">, "aria-label"> & {
|
|
35
|
+
/** 这张排期的名称。横向能滚动时,滚动的那一块用它 */
|
|
36
|
+
label: string;
|
|
37
|
+
/** 第一天,`YYYY-MM-DD` */
|
|
38
|
+
start: string;
|
|
39
|
+
/** 最后一天(包含) */
|
|
40
|
+
end: string;
|
|
41
|
+
/**
|
|
42
|
+
* 哪几天是日期锚:压在轴条上的日期块,往下带一条引线。
|
|
43
|
+
* 默认是第一天和范围内每个月的 1 号
|
|
44
|
+
*/
|
|
45
|
+
anchors?: readonly string[];
|
|
46
|
+
/** 今天:画一条竖线。不传就不画 */
|
|
47
|
+
today?: string;
|
|
48
|
+
/** 一天至少多宽,像素。默认 32。容器够宽时各列平分多出来的宽度 */
|
|
49
|
+
dayWidth?: number;
|
|
50
|
+
/** 读屏听到的日期按它写。默认 `zh-CN` */
|
|
51
|
+
locale?: string;
|
|
52
|
+
/**
|
|
53
|
+
* 读屏听到的起止日期怎么说。默认"10月3日至10月12日",只有一天时是"10月5日"。
|
|
54
|
+
* 换了 `locale` 一般也要换它
|
|
55
|
+
*/
|
|
56
|
+
describeRange?: (start: string, end: string) => string;
|
|
57
|
+
/** `ScheduleTrack` */
|
|
58
|
+
children?: ReactNode;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
62
|
+
|
|
63
|
+
/* 轴、引线、各条轨共用同一份列:一天一列 */
|
|
64
|
+
const dayColumns =
|
|
65
|
+
"grid grid-cols-[repeat(var(--schedule-days),minmax(var(--schedule-day),1fr))]";
|
|
66
|
+
|
|
67
|
+
/* 类目竖轨的宽度。轴和引线要从它右边起 */
|
|
68
|
+
const RAIL = "w-12";
|
|
69
|
+
const AFTER_RAIL = "left-12";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 排期:横向的时间轨图。条目从起始日的位置开始,长度就是持续时间。
|
|
73
|
+
* 最小单位是天。放不下时在自己的容器里横向滚动。
|
|
74
|
+
*/
|
|
75
|
+
export function Schedule({
|
|
76
|
+
label,
|
|
77
|
+
start,
|
|
78
|
+
end,
|
|
79
|
+
anchors: anchorsProp,
|
|
80
|
+
today,
|
|
81
|
+
dayWidth = 32,
|
|
82
|
+
locale = "zh-CN",
|
|
83
|
+
describeRange,
|
|
84
|
+
className,
|
|
85
|
+
style,
|
|
86
|
+
children,
|
|
87
|
+
...props
|
|
88
|
+
}: ScheduleProps) {
|
|
89
|
+
const [scrollerRef, overflowing] = useOverflowing();
|
|
90
|
+
const dates = useMemo(() => eachDay(start, end), [start, end]);
|
|
91
|
+
const days = dates.length;
|
|
92
|
+
|
|
93
|
+
const anchors = useMemo(
|
|
94
|
+
() =>
|
|
95
|
+
new Set(
|
|
96
|
+
anchorsProp ??
|
|
97
|
+
dates.filter(
|
|
98
|
+
(date, position) => position === 0 || dateParts(date).day === 1,
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
[anchorsProp, dates],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const context = useMemo(() => {
|
|
105
|
+
const day = new Intl.DateTimeFormat(locale, {
|
|
106
|
+
timeZone: "UTC",
|
|
107
|
+
month: "long",
|
|
108
|
+
day: "numeric",
|
|
109
|
+
});
|
|
110
|
+
const say = (date: string) => day.format(toDateObject(date));
|
|
111
|
+
return {
|
|
112
|
+
start,
|
|
113
|
+
days,
|
|
114
|
+
describe:
|
|
115
|
+
describeRange ??
|
|
116
|
+
((from: string, to: string) =>
|
|
117
|
+
from === to ? say(from) : `${say(from)}至${say(to)}`),
|
|
118
|
+
};
|
|
119
|
+
}, [start, days, locale, describeRange]);
|
|
120
|
+
|
|
121
|
+
const column = (date: string) => daysBetween(start, date) + 1;
|
|
122
|
+
const todayColumn =
|
|
123
|
+
today !== undefined && today >= start && today <= end
|
|
124
|
+
? column(today)
|
|
125
|
+
: null;
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<ScheduleContext value={context}>
|
|
129
|
+
<div
|
|
130
|
+
ref={scrollerRef}
|
|
131
|
+
{...props}
|
|
132
|
+
data-overflowing={overflowing.x ? "" : undefined}
|
|
133
|
+
// 只有真的能横向滚动时才是一个能聚焦的区域(同表格)
|
|
134
|
+
{...(overflowing.x && {
|
|
135
|
+
role: "region",
|
|
136
|
+
"aria-label": label,
|
|
137
|
+
tabIndex: 0,
|
|
138
|
+
})}
|
|
139
|
+
style={
|
|
140
|
+
{
|
|
141
|
+
"--schedule-days": days,
|
|
142
|
+
"--schedule-day": `${dayWidth}px`,
|
|
143
|
+
...style,
|
|
144
|
+
} as CSSProperties
|
|
145
|
+
}
|
|
146
|
+
className={cn(
|
|
147
|
+
"isolate overflow-x-auto text-ink [--schedule-surface:var(--ef-surface)]",
|
|
148
|
+
focusRingInset,
|
|
149
|
+
className,
|
|
150
|
+
)}
|
|
151
|
+
>
|
|
152
|
+
{/*
|
|
153
|
+
* 宽度写死成"竖轨 + 天数 × 每天的宽度",比容器窄时撑满容器。
|
|
154
|
+
* 不能让它跟着内容走:名称长的条目会把自己那几列撑宽,位置就不准了
|
|
155
|
+
*/}
|
|
156
|
+
<div
|
|
157
|
+
role={overflowing.x ? undefined : "group"}
|
|
158
|
+
aria-label={overflowing.x ? undefined : label}
|
|
159
|
+
className="relative w-[calc(3rem+var(--schedule-days)*var(--schedule-day))] min-w-full"
|
|
160
|
+
>
|
|
161
|
+
{/* 空档:没有条目的时段露出这层淡斜纹 */}
|
|
162
|
+
<div
|
|
163
|
+
aria-hidden="true"
|
|
164
|
+
className={cn(
|
|
165
|
+
decor,
|
|
166
|
+
"absolute inset-y-0 right-0 bg-surface-sunken hatch",
|
|
167
|
+
AFTER_RAIL,
|
|
168
|
+
)}
|
|
169
|
+
/>
|
|
170
|
+
|
|
171
|
+
{/* 引线:从每个日期锚往下,画在条目后面 */}
|
|
172
|
+
<div
|
|
173
|
+
aria-hidden="true"
|
|
174
|
+
className={cn(
|
|
175
|
+
decor,
|
|
176
|
+
"absolute inset-y-0 right-0",
|
|
177
|
+
AFTER_RAIL,
|
|
178
|
+
dayColumns,
|
|
179
|
+
)}
|
|
180
|
+
>
|
|
181
|
+
{[...anchors].map((date) => (
|
|
182
|
+
<span
|
|
183
|
+
key={date}
|
|
184
|
+
style={{ gridColumn: column(date) }}
|
|
185
|
+
className="row-start-1 border-l border-dashed border-line-strong"
|
|
186
|
+
/>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{/* 时间轴 */}
|
|
191
|
+
<div aria-hidden="true" className="relative flex">
|
|
192
|
+
{/* 左上角:盖住滚过去的日号 */}
|
|
193
|
+
<div
|
|
194
|
+
className={cn(
|
|
195
|
+
"sticky left-0 z-3 shrink-0 bg-(--schedule-surface)",
|
|
196
|
+
RAIL,
|
|
197
|
+
)}
|
|
198
|
+
/>
|
|
199
|
+
<div
|
|
200
|
+
className={cn(
|
|
201
|
+
"h-8 flex-1 items-center bg-surface-muted",
|
|
202
|
+
dayColumns,
|
|
203
|
+
)}
|
|
204
|
+
>
|
|
205
|
+
{dates.map((date, position) => {
|
|
206
|
+
if (anchors.has(date)) {
|
|
207
|
+
const { month, day } = dateParts(date);
|
|
208
|
+
return (
|
|
209
|
+
<Tag
|
|
210
|
+
key={date}
|
|
211
|
+
variant="inverse"
|
|
212
|
+
size="sm"
|
|
213
|
+
numeric
|
|
214
|
+
marked
|
|
215
|
+
data-anchor={date}
|
|
216
|
+
// 日期块比一天的格子宽:从这一格的左缘起,压到后面的格子上
|
|
217
|
+
className="z-1 justify-self-start"
|
|
218
|
+
>
|
|
219
|
+
{`${pad(month)}.${pad(day)}`}
|
|
220
|
+
</Tag>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
// 被前一个日期块压住的那一格不写日号
|
|
224
|
+
const covered =
|
|
225
|
+
position > 0 && anchors.has(dates[position - 1]!);
|
|
226
|
+
return (
|
|
227
|
+
<span
|
|
228
|
+
key={date}
|
|
229
|
+
className="text-center font-tech text-micro text-ink-tertiary tabular-nums"
|
|
230
|
+
>
|
|
231
|
+
{covered ? null : dateParts(date).day}
|
|
232
|
+
</span>
|
|
233
|
+
);
|
|
234
|
+
})}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div className="relative flex flex-col gap-1 pt-1">{children}</div>
|
|
239
|
+
|
|
240
|
+
{/* 今天:一条竖线,画在条目上面 */}
|
|
241
|
+
{todayColumn !== null && (
|
|
242
|
+
<div
|
|
243
|
+
aria-hidden="true"
|
|
244
|
+
className={cn(
|
|
245
|
+
decor,
|
|
246
|
+
"absolute inset-y-0 right-0 z-2",
|
|
247
|
+
AFTER_RAIL,
|
|
248
|
+
dayColumns,
|
|
249
|
+
)}
|
|
250
|
+
>
|
|
251
|
+
<span
|
|
252
|
+
data-today=""
|
|
253
|
+
style={{ gridColumn: todayColumn }}
|
|
254
|
+
className="row-start-1 border-l-2 border-accent-ink"
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</ScheduleContext>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export type ScheduleTrackProps = Omit<
|
|
265
|
+
ComponentProps<"section">,
|
|
266
|
+
"aria-label"
|
|
267
|
+
> & {
|
|
268
|
+
/** 类目的名称:竖轨上的字,也是这条轨的可访问名称 */
|
|
269
|
+
label: string;
|
|
270
|
+
/** 竖轨上的图标 */
|
|
271
|
+
icon?: ReactNode;
|
|
272
|
+
/** 直接放 `ScheduleItem` */
|
|
273
|
+
children?: ReactNode;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 一条轨:一个类目的条目排在一行里,时间重叠的自动错到下一行。
|
|
278
|
+
* 竖轨的颜色用 `className="[--track-color:…] [--track-ink:…]"` 换。
|
|
279
|
+
*/
|
|
280
|
+
export function ScheduleTrack({
|
|
281
|
+
label,
|
|
282
|
+
icon,
|
|
283
|
+
className,
|
|
284
|
+
children,
|
|
285
|
+
...props
|
|
286
|
+
}: ScheduleTrackProps) {
|
|
287
|
+
useSchedule("ScheduleTrack");
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<section
|
|
291
|
+
{...props}
|
|
292
|
+
aria-label={label}
|
|
293
|
+
className={cn(
|
|
294
|
+
"flex [--track-color:var(--ef-surface-inverse)] [--track-ink:var(--ef-ink-inverse)]",
|
|
295
|
+
className,
|
|
296
|
+
)}
|
|
297
|
+
>
|
|
298
|
+
{/* 竖轨冻结在左边:底不透明,滚过去的条目才不会透出来 */}
|
|
299
|
+
<div
|
|
300
|
+
aria-hidden="true"
|
|
301
|
+
className={cn(
|
|
302
|
+
"sticky left-0 z-3 flex shrink-0 flex-col items-center gap-2 bg-(--track-color) py-2 text-(--track-ink)",
|
|
303
|
+
// 和右边的条目之间留一条表面色的缝
|
|
304
|
+
"border-r-4 border-(--schedule-surface)",
|
|
305
|
+
RAIL,
|
|
306
|
+
)}
|
|
307
|
+
>
|
|
308
|
+
{icon && (
|
|
309
|
+
<span className="flex size-4 shrink-0 items-center justify-center [&_svg]:size-4">
|
|
310
|
+
{icon}
|
|
311
|
+
</span>
|
|
312
|
+
)}
|
|
313
|
+
<span className="text-xs leading-none font-medium tracking-label [writing-mode:vertical-rl]">
|
|
314
|
+
{label}
|
|
315
|
+
</span>
|
|
316
|
+
</div>
|
|
317
|
+
<ul
|
|
318
|
+
className={cn(
|
|
319
|
+
"min-h-12 flex-1 grid-flow-row-dense auto-rows-[3rem] gap-y-1",
|
|
320
|
+
dayColumns,
|
|
321
|
+
)}
|
|
322
|
+
>
|
|
323
|
+
{children}
|
|
324
|
+
</ul>
|
|
325
|
+
</section>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export type ScheduleItemVariant = "banner" | "system";
|
|
330
|
+
|
|
331
|
+
type ItemOwnProps = {
|
|
332
|
+
/** 第一天,`YYYY-MM-DD` */
|
|
333
|
+
start: string;
|
|
334
|
+
/** 最后一天(包含) */
|
|
335
|
+
end: string;
|
|
336
|
+
/** 名称 */
|
|
337
|
+
title: ReactNode;
|
|
338
|
+
/** 类型小标签:右上角的白底描边小块。只有 `banner` 有 */
|
|
339
|
+
type?: ReactNode;
|
|
340
|
+
/**
|
|
341
|
+
* - `banner` 活动:浅底,可以垫一张图,左上是墨色的名称块,默认;
|
|
342
|
+
* - `system` 系统类:炭灰底 + 行动色侧签 + 一条虚线,不带图。
|
|
343
|
+
*/
|
|
344
|
+
variant?: ScheduleItemVariant;
|
|
345
|
+
/** 垫在条目下面的图,铺满并裁切。只有 `banner` 有 */
|
|
346
|
+
media?: ReactNode;
|
|
347
|
+
/** 传了名称就是一个链接,点击范围铺满整个条目 */
|
|
348
|
+
href?: string;
|
|
349
|
+
target?: string;
|
|
350
|
+
rel?: string;
|
|
351
|
+
/** 用这个元素代替 `<a>`(路由库的链接组件)。传了就按链接处理 */
|
|
352
|
+
render?: LinkRender;
|
|
353
|
+
onClick?: (event: MouseEvent<HTMLElement>) => void;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export type ScheduleItemProps = ItemOwnProps &
|
|
357
|
+
Omit<ComponentProps<"li">, keyof ItemOwnProps | "children">;
|
|
358
|
+
|
|
359
|
+
/** 排期里的一个条目。直接放在 `ScheduleTrack` 里。 */
|
|
360
|
+
export function ScheduleItem({
|
|
361
|
+
start,
|
|
362
|
+
end,
|
|
363
|
+
title,
|
|
364
|
+
type,
|
|
365
|
+
variant = "banner",
|
|
366
|
+
media,
|
|
367
|
+
href,
|
|
368
|
+
target,
|
|
369
|
+
rel,
|
|
370
|
+
render,
|
|
371
|
+
onClick,
|
|
372
|
+
className,
|
|
373
|
+
style,
|
|
374
|
+
...props
|
|
375
|
+
}: ScheduleItemProps) {
|
|
376
|
+
const schedule = useSchedule("ScheduleItem");
|
|
377
|
+
const first = daysBetween(schedule.start, start);
|
|
378
|
+
const last = daysBetween(schedule.start, end);
|
|
379
|
+
// 整个都在范围外:不画
|
|
380
|
+
if (last < 0 || first >= schedule.days || last < first) return null;
|
|
381
|
+
|
|
382
|
+
// 超出范围的两头裁到范围内
|
|
383
|
+
const from = Math.max(first, 0) + 1;
|
|
384
|
+
const to = Math.min(last, schedule.days - 1) + 2;
|
|
385
|
+
const isLink = href !== undefined || render !== undefined;
|
|
386
|
+
const banner = variant === "banner";
|
|
387
|
+
|
|
388
|
+
const name = isLink ? (
|
|
389
|
+
<LinkElement
|
|
390
|
+
render={render}
|
|
391
|
+
href={href}
|
|
392
|
+
target={target}
|
|
393
|
+
rel={rel}
|
|
394
|
+
onClick={onClick}
|
|
395
|
+
// 点击范围铺满整个条目;焦点环画在条目上
|
|
396
|
+
className="outline-none after:absolute after:inset-0 after:content-['']"
|
|
397
|
+
>
|
|
398
|
+
{title}
|
|
399
|
+
</LinkElement>
|
|
400
|
+
) : (
|
|
401
|
+
title
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
return (
|
|
405
|
+
<li
|
|
406
|
+
{...props}
|
|
407
|
+
data-variant={variant}
|
|
408
|
+
data-clipped-start={first < 0 ? "" : undefined}
|
|
409
|
+
data-clipped-end={last >= schedule.days ? "" : undefined}
|
|
410
|
+
style={{ gridColumn: `${from} / ${to}`, ...style }}
|
|
411
|
+
className={cn(
|
|
412
|
+
// 条目自己是一个容器:窄到放不下类型标签时把它收掉
|
|
413
|
+
"@container relative isolate flex min-w-0 overflow-clip text-sm",
|
|
414
|
+
"has-focus-visible:outline-2 has-focus-visible:-outline-offset-2 has-focus-visible:outline-focus",
|
|
415
|
+
banner
|
|
416
|
+
? "items-start justify-between gap-2 bg-surface-muted p-2"
|
|
417
|
+
: // 系统类条目在两个主题下都是炭灰底:control 系列本来就不随主题变
|
|
418
|
+
"items-center gap-2 bg-control pr-2 text-on-control",
|
|
419
|
+
className,
|
|
420
|
+
)}
|
|
421
|
+
>
|
|
422
|
+
{banner ? (
|
|
423
|
+
<>
|
|
424
|
+
{media && (
|
|
425
|
+
<div
|
|
426
|
+
aria-hidden="true"
|
|
427
|
+
className="absolute inset-0 -z-10 *:size-full *:object-cover"
|
|
428
|
+
>
|
|
429
|
+
{media}
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
<span className="flex h-6 max-w-full min-w-0 items-center bg-surface-inverse px-2 font-medium text-ink-inverse">
|
|
433
|
+
<span className="truncate">{name}</span>
|
|
434
|
+
</span>
|
|
435
|
+
{type && (
|
|
436
|
+
<Tag variant="outline" size="sm" className="@max-[10rem]:hidden">
|
|
437
|
+
{type}
|
|
438
|
+
</Tag>
|
|
439
|
+
)}
|
|
440
|
+
</>
|
|
441
|
+
) : (
|
|
442
|
+
<>
|
|
443
|
+
<span
|
|
444
|
+
aria-hidden="true"
|
|
445
|
+
className="w-1.5 shrink-0 self-stretch bg-action"
|
|
446
|
+
/>
|
|
447
|
+
<span className="min-w-0 truncate font-medium">{name}</span>
|
|
448
|
+
<span
|
|
449
|
+
aria-hidden="true"
|
|
450
|
+
className="min-w-4 flex-1 border-t border-dashed border-action"
|
|
451
|
+
/>
|
|
452
|
+
</>
|
|
453
|
+
)}
|
|
454
|
+
<span className="sr-only">{`,${schedule.describe(start, end)}`}</span>
|
|
455
|
+
</li>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ScrollArea as BaseScrollArea } from "@base-ui/react/scroll-area";
|
|
2
|
+
import type { ComponentProps, CSSProperties, Ref } from "react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { focusRingInset } from "../../lib/focus-ring";
|
|
5
|
+
|
|
6
|
+
export type ScrollAreaOrientation = "vertical" | "horizontal" | "both";
|
|
7
|
+
|
|
8
|
+
export type ScrollAreaProps = Omit<
|
|
9
|
+
ComponentProps<"div">,
|
|
10
|
+
"aria-label" | "className" | "style" | "ref"
|
|
11
|
+
> & {
|
|
12
|
+
/** 这一块是什么。能滚的时候它是一个能聚焦的区域,读屏听到的就是这个名称 */
|
|
13
|
+
"aria-label": string;
|
|
14
|
+
/** 哪个方向能滚,默认 `vertical`。只管一个方向时,另一个方向滚不了 */
|
|
15
|
+
orientation?: ScrollAreaOrientation;
|
|
16
|
+
/** 真正在滚的那个元素:给页内目录、回到顶部的 `target` */
|
|
17
|
+
viewportRef?: Ref<HTMLDivElement>;
|
|
18
|
+
/** 给外框:高度或者高度上限写在这里 */
|
|
19
|
+
className?: string;
|
|
20
|
+
style?: CSSProperties;
|
|
21
|
+
/** 给外框 */
|
|
22
|
+
ref?: Ref<HTMLDivElement>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* 滚动条:12px 宽的一条是能抓、能点的范围;正中一条 1px 的轨,
|
|
27
|
+
* 滑块是压在轨上的一段 3px 的粗条——和页内目录、侧轨二级的引线是同一个画法
|
|
28
|
+
*/
|
|
29
|
+
const barClass: Record<"vertical" | "horizontal", string> = {
|
|
30
|
+
vertical:
|
|
31
|
+
"w-3 before:absolute before:inset-y-0 before:left-1/2 before:w-px before:-translate-x-1/2 before:bg-line",
|
|
32
|
+
horizontal:
|
|
33
|
+
"h-3 before:absolute before:inset-x-0 before:top-1/2 before:h-px before:-translate-y-1/2 before:bg-line",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const thumbClass: Record<"vertical" | "horizontal", string> = {
|
|
37
|
+
vertical:
|
|
38
|
+
"w-full before:inset-y-0 before:left-1/2 before:w-[3px] before:-translate-x-1/2",
|
|
39
|
+
horizontal:
|
|
40
|
+
"h-full before:inset-x-0 before:top-1/2 before:h-[3px] before:-translate-y-1/2",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function Scrollbar({
|
|
44
|
+
orientation,
|
|
45
|
+
}: {
|
|
46
|
+
orientation: "vertical" | "horizontal";
|
|
47
|
+
}) {
|
|
48
|
+
return (
|
|
49
|
+
<BaseScrollArea.Scrollbar
|
|
50
|
+
orientation={orientation}
|
|
51
|
+
className={cn("group/bar flex", barClass[orientation])}
|
|
52
|
+
>
|
|
53
|
+
<BaseScrollArea.Thumb
|
|
54
|
+
className={cn(
|
|
55
|
+
"relative before:absolute before:bg-ink-secondary before:content-['']",
|
|
56
|
+
"before:transition-colors before:duration-(--duration-fast) before:ease-standard",
|
|
57
|
+
"group-hover/bar:before:bg-ink active:before:bg-ink",
|
|
58
|
+
// 高对比模式下背景色会被系统拿掉:换成系统的文字色,滑块才看得见
|
|
59
|
+
"forced-colors:before:bg-[CanvasText]",
|
|
60
|
+
thumbClass[orientation],
|
|
61
|
+
)}
|
|
62
|
+
/>
|
|
63
|
+
</BaseScrollArea.Scrollbar>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 滚动区:一块自己能滚的区域,滚动条是画出来的。高度(或高度上限)由 `className` 给。
|
|
69
|
+
* 只有真的溢出时才有滚动条,也才是一个能聚焦的区域。
|
|
70
|
+
*
|
|
71
|
+
* 整页的滚动留给浏览器,不要把页面主体包进来。
|
|
72
|
+
* `className` 与 `ref` 给外框,其余属性(`onScroll` 等)给真正在滚的可视区。
|
|
73
|
+
*/
|
|
74
|
+
export function ScrollArea({
|
|
75
|
+
"aria-label": label,
|
|
76
|
+
orientation = "vertical",
|
|
77
|
+
viewportRef,
|
|
78
|
+
className,
|
|
79
|
+
style,
|
|
80
|
+
ref,
|
|
81
|
+
children,
|
|
82
|
+
...props
|
|
83
|
+
}: ScrollAreaProps) {
|
|
84
|
+
const vertical = orientation !== "horizontal";
|
|
85
|
+
const horizontal = orientation !== "vertical";
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<BaseScrollArea.Root
|
|
89
|
+
ref={ref}
|
|
90
|
+
style={style}
|
|
91
|
+
data-scroll-area=""
|
|
92
|
+
data-orientation={orientation}
|
|
93
|
+
// 给了高度上限时内容短就跟着矮:外框是一个竖排的弹性容器,可视区在里面收缩
|
|
94
|
+
className={cn("flex min-h-0 min-w-0 flex-col text-ink", className)}
|
|
95
|
+
>
|
|
96
|
+
<BaseScrollArea.Viewport
|
|
97
|
+
{...props}
|
|
98
|
+
ref={viewportRef}
|
|
99
|
+
// 不管的那个方向不让滚(基元默认两个方向都是 scroll)
|
|
100
|
+
style={
|
|
101
|
+
orientation === "vertical"
|
|
102
|
+
? { overflowX: "hidden" }
|
|
103
|
+
: orientation === "horizontal"
|
|
104
|
+
? { overflowY: "hidden" }
|
|
105
|
+
: undefined
|
|
106
|
+
}
|
|
107
|
+
className={cn(
|
|
108
|
+
"min-h-0 flex-auto",
|
|
109
|
+
focusRingInset,
|
|
110
|
+
// 溢出的那一侧让出滚动条的位置,内容不会被压住。让的是可视区自己的外边距:
|
|
111
|
+
// 写成内边距的话,它在滚动容器里是"滚到头才有的留白",途中内容照样被压住
|
|
112
|
+
vertical && "data-has-overflow-y:mr-3",
|
|
113
|
+
horizontal && "data-has-overflow-x:mb-3",
|
|
114
|
+
)}
|
|
115
|
+
render={(viewportProps, state) => {
|
|
116
|
+
const scrollable =
|
|
117
|
+
(vertical && state.hasOverflowY) ||
|
|
118
|
+
(horizontal && state.hasOverflowX);
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
{...viewportProps}
|
|
122
|
+
// 只有真的能滚时才是一个有名称、能聚焦的区域:键盘要能滚它,
|
|
123
|
+
// 但不溢出的时候不该白占一个 Tab 停靠点
|
|
124
|
+
{...(scrollable && { role: "region", "aria-label": label })}
|
|
125
|
+
tabIndex={scrollable ? 0 : -1}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<BaseScrollArea.Content>{children}</BaseScrollArea.Content>
|
|
131
|
+
</BaseScrollArea.Viewport>
|
|
132
|
+
{vertical && <Scrollbar orientation="vertical" />}
|
|
133
|
+
{horizontal && <Scrollbar orientation="horizontal" />}
|
|
134
|
+
{orientation === "both" && <BaseScrollArea.Corner />}
|
|
135
|
+
</BaseScrollArea.Root>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
type ComponentProps,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
type RefObject,
|
|
6
|
+
} from "react";
|
|
7
|
+
import {
|
|
8
|
+
useScrollPosition,
|
|
9
|
+
type ScrollMetrics,
|
|
10
|
+
} from "../../hooks/useScrollPosition";
|
|
11
|
+
import { ChevronDown } from "../../icons/ChevronDown";
|
|
12
|
+
import { cn } from "../../lib/cn";
|
|
13
|
+
|
|
14
|
+
export type ScrollHintVariant = "line" | "chevron";
|
|
15
|
+
|
|
16
|
+
export type ScrollHintProps = Omit<ComponentProps<"div">, "children"> & {
|
|
17
|
+
/** 上方的小字,默认 `SCROLL` */
|
|
18
|
+
label?: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* - `line` 一条细竖线 + 一个下坠的光点,默认;
|
|
21
|
+
* - `chevron` 一个下坠的折线箭头,窄屏用。
|
|
22
|
+
*/
|
|
23
|
+
variant?: ScrollHintVariant;
|
|
24
|
+
/** 看哪个滚动容器。不传就是整个页面 */
|
|
25
|
+
target?: RefObject<HTMLElement | null>;
|
|
26
|
+
/** 滚过多少像素算"已经开始滚动",默认 8 */
|
|
27
|
+
threshold?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 滚动提示:只在确实还能往下滚、并且还没开始滚的时候出现,滚动开始后淡出。
|
|
32
|
+
* 纯装饰,对读屏隐藏;位置由使用方定(首屏底部居中或右下)。
|
|
33
|
+
*/
|
|
34
|
+
export function ScrollHint({
|
|
35
|
+
label = "SCROLL",
|
|
36
|
+
variant = "line",
|
|
37
|
+
target,
|
|
38
|
+
threshold = 8,
|
|
39
|
+
className,
|
|
40
|
+
...props
|
|
41
|
+
}: ScrollHintProps) {
|
|
42
|
+
// 确实还能往下滚,并且还没开始滚。指定了容器但它还没挂上时不显示
|
|
43
|
+
const stillAtTop = useCallback(
|
|
44
|
+
({ scrolled, room }: ScrollMetrics) =>
|
|
45
|
+
room > threshold && scrolled <= threshold,
|
|
46
|
+
[threshold],
|
|
47
|
+
);
|
|
48
|
+
const visible = useScrollPosition(target, stillAtTop, false);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
{...props}
|
|
54
|
+
data-visible={visible ? "" : undefined}
|
|
55
|
+
className={cn(
|
|
56
|
+
"pointer-events-none inline-flex flex-col items-center gap-2 text-ink-secondary",
|
|
57
|
+
"transition-opacity duration-(--duration-base) ease-standard",
|
|
58
|
+
visible ? "opacity-100" : "opacity-0",
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
<span className="font-tech text-micro tracking-label uppercase">
|
|
63
|
+
{label}
|
|
64
|
+
</span>
|
|
65
|
+
{variant === "line" ? (
|
|
66
|
+
<span className="relative h-10 w-px bg-line-strong">
|
|
67
|
+
{/* 光点骑在线上:6px 的点对 1px 的线,向左偏 2.5px */}
|
|
68
|
+
<span className="absolute top-0 -left-[2.5px] size-1.5 animate-scroll-hint rounded-full bg-accent-ink" />
|
|
69
|
+
</span>
|
|
70
|
+
) : (
|
|
71
|
+
<ChevronDown
|
|
72
|
+
size={16}
|
|
73
|
+
className="animate-scroll-hint text-accent-ink"
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|