@endfield-ui/react 0.0.0-stage → 0.1.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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +27 -0
  3. package/README.md +768 -2
  4. package/dist/index.d.ts +3243 -0
  5. package/dist/index.js +8182 -0
  6. package/dist/styles.css +2 -0
  7. package/package.json +83 -3
  8. package/src/components/README.md +172 -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 +89 -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 +292 -0
  30. package/src/components/carousel/index.ts +7 -0
  31. package/src/components/checkbox/Checkbox.tsx +94 -0
  32. package/src/components/checkbox/choice-style.ts +30 -0
  33. package/src/components/checkbox/index.ts +1 -0
  34. package/src/components/chip/FilterChip.tsx +68 -0
  35. package/src/components/chip/ResourceChip.tsx +63 -0
  36. package/src/components/chip/capsule-style.ts +22 -0
  37. package/src/components/chip/index.ts +6 -0
  38. package/src/components/combobox/Combobox.tsx +450 -0
  39. package/src/components/combobox/index.ts +10 -0
  40. package/src/components/completion-banner/CompletionBanner.tsx +72 -0
  41. package/src/components/completion-banner/index.ts +4 -0
  42. package/src/components/context-menu/ContextMenu.tsx +70 -0
  43. package/src/components/context-menu/index.ts +5 -0
  44. package/src/components/corner-brackets/CornerBrackets.tsx +43 -0
  45. package/src/components/corner-brackets/index.ts +5 -0
  46. package/src/components/countdown/Countdown.tsx +145 -0
  47. package/src/components/countdown/index.ts +6 -0
  48. package/src/components/dash-indicator/DashIndicator.tsx +44 -0
  49. package/src/components/dash-indicator/index.ts +1 -0
  50. package/src/components/data-row/DataRow.tsx +282 -0
  51. package/src/components/data-row/index.ts +8 -0
  52. package/src/components/date-picker/DateField.tsx +159 -0
  53. package/src/components/date-picker/DatePicker.tsx +182 -0
  54. package/src/components/date-picker/DateRangePicker.tsx +203 -0
  55. package/src/components/date-picker/index.ts +7 -0
  56. package/src/components/dialog/Dialog.tsx +203 -0
  57. package/src/components/dialog/index.ts +7 -0
  58. package/src/components/dialog/initial-focus.ts +19 -0
  59. package/src/components/dialog/overlay-style.ts +28 -0
  60. package/src/components/drawer/Drawer.tsx +213 -0
  61. package/src/components/drawer/index.ts +6 -0
  62. package/src/components/dropdown-menu/DropdownMenu.tsx +427 -0
  63. package/src/components/dropdown-menu/MenuCheck.tsx +44 -0
  64. package/src/components/dropdown-menu/index.ts +22 -0
  65. package/src/components/dropdown-menu/menu-style.ts +47 -0
  66. package/src/components/empty-state/EmptyState.tsx +79 -0
  67. package/src/components/empty-state/index.ts +1 -0
  68. package/src/components/field/Field.tsx +203 -0
  69. package/src/components/field/index.ts +6 -0
  70. package/src/components/file-upload/FileItem.tsx +201 -0
  71. package/src/components/file-upload/FileUpload.tsx +425 -0
  72. package/src/components/file-upload/index.ts +9 -0
  73. package/src/components/flyout-bar/FlyoutBar.tsx +158 -0
  74. package/src/components/flyout-bar/index.ts +7 -0
  75. package/src/components/ghost-text/GhostText.tsx +47 -0
  76. package/src/components/ghost-text/index.ts +5 -0
  77. package/src/components/hatch/Hatch.tsx +35 -0
  78. package/src/components/hatch/index.ts +1 -0
  79. package/src/components/hazard-stripe/HazardStripe.tsx +35 -0
  80. package/src/components/hazard-stripe/index.ts +5 -0
  81. package/src/components/icon-button/IconButton.tsx +95 -0
  82. package/src/components/icon-button/index.ts +6 -0
  83. package/src/components/input/Input.tsx +109 -0
  84. package/src/components/input/control-box.ts +71 -0
  85. package/src/components/input/index.ts +6 -0
  86. package/src/components/input/value-chip.ts +34 -0
  87. package/src/components/item-slot/ItemGrid.tsx +207 -0
  88. package/src/components/item-slot/ItemSlot.tsx +283 -0
  89. package/src/components/item-slot/index.ts +7 -0
  90. package/src/components/kbd/Kbd.tsx +33 -0
  91. package/src/components/kbd/index.ts +1 -0
  92. package/src/components/link-element/LinkElement.tsx +25 -0
  93. package/src/components/list/List.tsx +229 -0
  94. package/src/components/list/band-style.ts +18 -0
  95. package/src/components/list/index.ts +8 -0
  96. package/src/components/loader/Loader.tsx +154 -0
  97. package/src/components/loader/index.ts +1 -0
  98. package/src/components/marquee/Marquee.tsx +96 -0
  99. package/src/components/marquee/index.ts +1 -0
  100. package/src/components/media-card/MediaCard.tsx +139 -0
  101. package/src/components/media-card/index.ts +5 -0
  102. package/src/components/nav-action/NavAction.tsx +142 -0
  103. package/src/components/nav-action/index.ts +5 -0
  104. package/src/components/nav-menu/NavMenu.tsx +288 -0
  105. package/src/components/nav-menu/index.ts +6 -0
  106. package/src/components/navigator/Navigator.tsx +136 -0
  107. package/src/components/navigator/index.ts +5 -0
  108. package/src/components/pagination/Pagination.tsx +181 -0
  109. package/src/components/pagination/index.ts +5 -0
  110. package/src/components/panel/Panel.tsx +114 -0
  111. package/src/components/panel/index.ts +13 -0
  112. package/src/components/play-button/PlayButton.tsx +92 -0
  113. package/src/components/play-button/index.ts +7 -0
  114. package/src/components/popover/Popover.tsx +105 -0
  115. package/src/components/popover/index.ts +8 -0
  116. package/src/components/progress/Progress.tsx +133 -0
  117. package/src/components/progress/ProgressRing.tsx +123 -0
  118. package/src/components/progress/index.ts +2 -0
  119. package/src/components/radio/Radio.tsx +182 -0
  120. package/src/components/radio/index.ts +7 -0
  121. package/src/components/rec-indicator/RecIndicator.tsx +47 -0
  122. package/src/components/rec-indicator/index.ts +1 -0
  123. package/src/components/registration-strip/RegistrationStrip.tsx +51 -0
  124. package/src/components/registration-strip/index.ts +5 -0
  125. package/src/components/schedule/Schedule.tsx +457 -0
  126. package/src/components/schedule/index.ts +9 -0
  127. package/src/components/scroll-hint/ScrollHint.tsx +78 -0
  128. package/src/components/scroll-hint/index.ts +5 -0
  129. package/src/components/section-title/SectionTitle.tsx +202 -0
  130. package/src/components/section-title/index.ts +5 -0
  131. package/src/components/segmented-control/SegmentedControl.tsx +220 -0
  132. package/src/components/segmented-control/index.ts +8 -0
  133. package/src/components/select/Select.tsx +390 -0
  134. package/src/components/select/index.ts +15 -0
  135. package/src/components/side-rail/SideRail.tsx +317 -0
  136. package/src/components/side-rail/SideRailSub.tsx +298 -0
  137. package/src/components/side-rail/index.ts +14 -0
  138. package/src/components/skeleton/Skeleton.tsx +70 -0
  139. package/src/components/skeleton/index.ts +1 -0
  140. package/src/components/slider/Slider.tsx +219 -0
  141. package/src/components/slider/index.ts +8 -0
  142. package/src/components/sparkline/Sparkline.tsx +122 -0
  143. package/src/components/sparkline/index.ts +6 -0
  144. package/src/components/spinner/Spinner.tsx +64 -0
  145. package/src/components/spinner/index.ts +1 -0
  146. package/src/components/stat/Stat.tsx +79 -0
  147. package/src/components/stat/index.ts +1 -0
  148. package/src/components/stepper/Stepper.tsx +316 -0
  149. package/src/components/stepper/index.ts +1 -0
  150. package/src/components/steps/Steps.tsx +302 -0
  151. package/src/components/steps/index.ts +10 -0
  152. package/src/components/switch/Switch.tsx +120 -0
  153. package/src/components/switch/index.ts +1 -0
  154. package/src/components/table/Table.tsx +559 -0
  155. package/src/components/table/index.ts +20 -0
  156. package/src/components/tabs/Tabs.tsx +313 -0
  157. package/src/components/tabs/index.ts +12 -0
  158. package/src/components/tag/Tag.tsx +66 -0
  159. package/src/components/tag/TagPair.tsx +34 -0
  160. package/src/components/tag/index.ts +2 -0
  161. package/src/components/tag-input/TagInput.tsx +501 -0
  162. package/src/components/tag-input/index.ts +8 -0
  163. package/src/components/term/Term.tsx +53 -0
  164. package/src/components/term/index.ts +1 -0
  165. package/src/components/textarea/Textarea.tsx +99 -0
  166. package/src/components/textarea/index.ts +1 -0
  167. package/src/components/texture/Texture.tsx +45 -0
  168. package/src/components/texture/index.ts +1 -0
  169. package/src/components/tick-ring/TickRing.tsx +77 -0
  170. package/src/components/tick-ring/index.ts +1 -0
  171. package/src/components/timeline/Timeline.tsx +84 -0
  172. package/src/components/timeline/index.ts +7 -0
  173. package/src/components/toast/Toast.tsx +257 -0
  174. package/src/components/toast/index.ts +10 -0
  175. package/src/components/toc/Toc.tsx +262 -0
  176. package/src/components/toc/index.ts +7 -0
  177. package/src/components/toolbar/Toolbar.tsx +389 -0
  178. package/src/components/toolbar/index.ts +17 -0
  179. package/src/components/tooltip/Tooltip.tsx +175 -0
  180. package/src/components/tooltip/index.ts +8 -0
  181. package/src/components/top-bar/TopBar.tsx +47 -0
  182. package/src/components/top-bar/index.ts +1 -0
  183. package/src/components/viewfinder/Viewfinder.tsx +97 -0
  184. package/src/components/viewfinder/index.ts +6 -0
  185. package/src/hooks/README.md +14 -0
  186. package/src/hooks/index.ts +4 -0
  187. package/src/hooks/useControllableState.ts +30 -0
  188. package/src/hooks/useInView.ts +44 -0
  189. package/src/hooks/useOverflowing.ts +28 -0
  190. package/src/hooks/usePortalScope.ts +68 -0
  191. package/src/hooks/useScrollPosition.ts +57 -0
  192. package/src/hooks/useTheme.ts +102 -0
  193. package/src/icons/ArrowCorner.tsx +7 -0
  194. package/src/icons/ArrowLeft.tsx +6 -0
  195. package/src/icons/ArrowRight.tsx +6 -0
  196. package/src/icons/CalendarIcon.tsx +10 -0
  197. package/src/icons/Check.tsx +4 -0
  198. package/src/icons/ChevronDown.tsx +3 -0
  199. package/src/icons/ChevronLeft.tsx +6 -0
  200. package/src/icons/ChevronRight.tsx +6 -0
  201. package/src/icons/Close.tsx +3 -0
  202. package/src/icons/Crosshair.tsx +7 -0
  203. package/src/icons/Lock.tsx +6 -0
  204. package/src/icons/Menu.tsx +4 -0
  205. package/src/icons/Minus.tsx +4 -0
  206. package/src/icons/Plus.tsx +3 -0
  207. package/src/icons/README.md +31 -0
  208. package/src/icons/StatusDanger.tsx +7 -0
  209. package/src/icons/StatusInfo.tsx +7 -0
  210. package/src/icons/StatusSuccess.tsx +10 -0
  211. package/src/icons/StatusWarning.tsx +7 -0
  212. package/src/icons/TriangleRight.tsx +7 -0
  213. package/src/icons/Upload.tsx +7 -0
  214. package/src/icons/createIcon.tsx +34 -0
  215. package/src/icons/index.ts +21 -0
  216. package/src/index.ts +82 -0
  217. package/src/lib/README.md +17 -0
  218. package/src/lib/cn.ts +34 -0
  219. package/src/lib/date.ts +125 -0
  220. package/src/lib/decor.ts +6 -0
  221. package/src/lib/defined.ts +9 -0
  222. package/src/lib/file.ts +61 -0
  223. package/src/lib/focus-ring.ts +11 -0
  224. package/src/lib/merge-refs.ts +11 -0
  225. package/src/styles/README.md +70 -0
  226. package/src/styles/index.css +25 -0
  227. package/src/styles/theme.css +568 -0
  228. package/src/styles/utilities.css +241 -0
@@ -0,0 +1,443 @@
1
+ import {
2
+ useEffect,
3
+ useId,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type ComponentProps,
8
+ type KeyboardEvent,
9
+ } from "react";
10
+ import { useControllableState } from "../../hooks/useControllableState";
11
+ import { ChevronLeft } from "../../icons/ChevronLeft";
12
+ import { ChevronRight } from "../../icons/ChevronRight";
13
+ import { cn } from "../../lib/cn";
14
+ import {
15
+ addDays,
16
+ addMonths,
17
+ addMonthsToMonth,
18
+ clampDate,
19
+ dateParts,
20
+ monthGrid,
21
+ monthOf,
22
+ orderDates,
23
+ startOfWeek,
24
+ toDateObject,
25
+ today as localToday,
26
+ } from "../../lib/date";
27
+ import { focusRingInset } from "../../lib/focus-ring";
28
+ import { IconButton } from "../icon-button/IconButton";
29
+
30
+ /** 一段日期:`[起, 止]`,两头都包含。只有一天时两个一样 */
31
+ export type DateRange = [start: string, end: string];
32
+
33
+ type CalendarBaseProps = Omit<
34
+ ComponentProps<"div">,
35
+ "defaultValue" | "onChange"
36
+ > & {
37
+ /** 现在显示的月份,`YYYY-MM`。不传时跟着选中的那一天(没选就是今天) */
38
+ month?: string;
39
+ defaultMonth?: string;
40
+ onMonthChange?: (month: string) => void;
41
+ /** 能选的最早一天 */
42
+ min?: string;
43
+ /** 能选的最晚一天 */
44
+ max?: string;
45
+ /** 返回真的日子不可选(比如周末、已经约满的日子) */
46
+ isDateDisabled?: (date: string) => boolean;
47
+ /** 一周从哪天开始:1 是星期一(默认),0 是星期日 */
48
+ weekStartsOn?: 0 | 1;
49
+ /** 月份标题、星期、读屏听到的日期名称按它走。默认 `zh-CN` */
50
+ locale?: string;
51
+ /** 哪一天算"今天"。默认取本地时间;服务端渲染或要一个固定的今天时自己传 */
52
+ today?: string;
53
+ previousLabel?: string;
54
+ nextLabel?: string;
55
+ };
56
+
57
+ export type CalendarSingleProps = CalendarBaseProps & {
58
+ range?: false;
59
+ /** 选中的那一天,`YYYY-MM-DD`;没选是 `null` */
60
+ value?: string | null;
61
+ defaultValue?: string | null;
62
+ onValueChange?: (value: string) => void;
63
+ startLabel?: never;
64
+ endLabel?: never;
65
+ pendingLabel?: never;
66
+ };
67
+
68
+ export type CalendarRangeProps = CalendarBaseProps & {
69
+ /** 选一段:点两下,第一下定起始日,第二下定结束日 */
70
+ range: true;
71
+ /** 选中的那一段,`[起, 止]`;没选是 `null` */
72
+ value?: DateRange | null;
73
+ defaultValue?: DateRange | null;
74
+ /** 只在一段选完时触发;后点的那天更早也会排成先后 */
75
+ onValueChange?: (value: DateRange) => void;
76
+ /** 读屏在两端那两天的名称后面听到的词,默认"起始日""结束日" */
77
+ startLabel?: string;
78
+ endLabel?: string;
79
+ /** 起始日定了之后的那句播报,默认"再选结束日" */
80
+ pendingLabel?: string;
81
+ };
82
+
83
+ export type CalendarProps = CalendarSingleProps | CalendarRangeProps;
84
+
85
+ type Selection = string | DateRange | null;
86
+
87
+ const UTC = { timeZone: "UTC" } as const;
88
+
89
+ /** 值有没有变:一段日期每次都是新数组,要按内容比 */
90
+ const keyOf = (value: Selection) =>
91
+ value === null ? "" : typeof value === "string" ? value : value.join("/");
92
+
93
+ /** 键盘把焦点挪到哪一天。返回 `null` 是这个键不归月历管 */
94
+ function moveFocus(
95
+ event: KeyboardEvent,
96
+ from: string,
97
+ weekStartsOn: 0 | 1,
98
+ ): string | null {
99
+ switch (event.key) {
100
+ case "ArrowLeft":
101
+ return addDays(from, -1);
102
+ case "ArrowRight":
103
+ return addDays(from, 1);
104
+ case "ArrowUp":
105
+ return addDays(from, -7);
106
+ case "ArrowDown":
107
+ return addDays(from, 7);
108
+ case "Home":
109
+ return startOfWeek(from, weekStartsOn);
110
+ case "End":
111
+ return addDays(startOfWeek(from, weekStartsOn), 6);
112
+ case "PageUp":
113
+ return addMonths(from, event.shiftKey ? -12 : -1);
114
+ case "PageDown":
115
+ return addMonths(from, event.shiftKey ? 12 : 1);
116
+ default:
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * 月历:选一天;加 `range` 是选一段。日期是 `YYYY-MM-DD` 的字符串。
123
+ * 要一个带面板的字段用 `DatePicker` / `DateRangePicker`。
124
+ */
125
+ export function Calendar({
126
+ range = false,
127
+ value: valueProp,
128
+ defaultValue = null,
129
+ onValueChange,
130
+ month: monthProp,
131
+ defaultMonth,
132
+ onMonthChange,
133
+ min,
134
+ max,
135
+ isDateDisabled,
136
+ weekStartsOn = 1,
137
+ locale = "zh-CN",
138
+ today: todayProp,
139
+ previousLabel = "上个月",
140
+ nextLabel = "下个月",
141
+ startLabel = "起始日",
142
+ endLabel = "结束日",
143
+ pendingLabel = "再选结束日",
144
+ className,
145
+ ...props
146
+ }: CalendarProps) {
147
+ const titleId = useId();
148
+ const gridRef = useRef<HTMLTableSectionElement>(null);
149
+ const [today] = useState(() => todayProp ?? localToday());
150
+ const currentDay = todayProp ?? today;
151
+
152
+ const [value, setValue] = useControllableState<Selection>({
153
+ value: valueProp,
154
+ defaultValue,
155
+ onChange: (next) => {
156
+ if (next !== null) {
157
+ (onValueChange as ((value: string | DateRange) => void) | undefined)?.(
158
+ next,
159
+ );
160
+ }
161
+ },
162
+ });
163
+ // 一段的两头;选一天时两头是同一天
164
+ const committed: DateRange | null =
165
+ value === null ? null : typeof value === "string" ? [value, value] : value;
166
+
167
+ // 选一段时:起始日定了、结束日还没定
168
+ const [anchor, setAnchor] = useState<string | null>(null);
169
+ // 指针停在哪一天(预览用);键盘的位置在 focused 里
170
+ const [hovered, setHovered] = useState<string | null>(null);
171
+
172
+ // 键盘停在哪一天:整个网格只有它能被 Tab 停到
173
+ const [focused, setFocused] = useState(() =>
174
+ clampDate(committed?.[0] ?? currentDay, min, max),
175
+ );
176
+ const [month, setMonth] = useControllableState({
177
+ value: monthProp,
178
+ defaultValue: defaultMonth ?? monthOf(focused),
179
+ onChange: onMonthChange,
180
+ });
181
+ // 键盘移动之后要把焦点交给新的那一格;鼠标翻月不抢焦点
182
+ const pendingFocus = useRef(false);
183
+
184
+ // 从外面换了值(受控):月历跟过去。自己选出来的不算——刚点的那天已经在眼前了
185
+ const valueKey = keyOf(value);
186
+ const lastKey = useRef(valueKey);
187
+ const start = committed?.[0] ?? null;
188
+ useEffect(() => {
189
+ if (valueKey === lastKey.current) return;
190
+ lastKey.current = valueKey;
191
+ setAnchor(null);
192
+ if (start === null) return;
193
+ setFocused(start);
194
+ setMonth(monthOf(start));
195
+ }, [valueKey, start, setMonth]);
196
+
197
+ useEffect(() => {
198
+ if (!pendingFocus.current) return;
199
+ pendingFocus.current = false;
200
+ gridRef.current
201
+ ?.querySelector<HTMLElement>(`[data-date="${focused}"]`)
202
+ ?.focus();
203
+ }, [focused, month]);
204
+
205
+ const weeks = useMemo(
206
+ () => monthGrid(month, weekStartsOn),
207
+ [month, weekStartsOn],
208
+ );
209
+
210
+ const formats = useMemo(
211
+ () => ({
212
+ title: new Intl.DateTimeFormat(locale, {
213
+ ...UTC,
214
+ year: "numeric",
215
+ month: "long",
216
+ }),
217
+ weekdayShort: new Intl.DateTimeFormat(locale, {
218
+ ...UTC,
219
+ weekday: "narrow",
220
+ }),
221
+ weekdayLong: new Intl.DateTimeFormat(locale, { ...UTC, weekday: "long" }),
222
+ day: new Intl.DateTimeFormat(locale, { ...UTC, dateStyle: "full" }),
223
+ }),
224
+ [locale],
225
+ );
226
+
227
+ const unavailable = (date: string) =>
228
+ (min !== undefined && date < min) ||
229
+ (max !== undefined && date > max) ||
230
+ (isDateDisabled?.(date) ?? false);
231
+
232
+ const focusDate = (date: string) => {
233
+ const next = clampDate(date, min, max);
234
+ pendingFocus.current = true;
235
+ setFocused(next);
236
+ if (monthOf(next) !== month) setMonth(monthOf(next));
237
+ };
238
+
239
+ const commit = (next: string | DateRange) => {
240
+ lastKey.current = keyOf(next);
241
+ setValue(next);
242
+ };
243
+
244
+ const select = (date: string) => {
245
+ if (unavailable(date)) return;
246
+ setFocused(date);
247
+ if (monthOf(date) !== month) setMonth(monthOf(date));
248
+ if (!range) {
249
+ commit(date);
250
+ } else if (anchor === null) {
251
+ setAnchor(date);
252
+ } else {
253
+ setAnchor(null);
254
+ commit(orderDates(anchor, date));
255
+ }
256
+ };
257
+
258
+ const turn = (months: number) => {
259
+ const next = addMonthsToMonth(month, months);
260
+ setMonth(next);
261
+ // 停靠点跟到新的月份里同一天(没有那一天就落在月末),否则 Tab 进不来
262
+ setFocused(clampDate(addMonths(focused, months), min, max));
263
+ };
264
+
265
+ const firstOfMonth = `${month}-01`;
266
+ const previousDisabled = min !== undefined && addDays(firstOfMonth, -1) < min;
267
+ const nextDisabled = max !== undefined && addMonths(firstOfMonth, 1) > max;
268
+
269
+ // 停靠点不在眼前这个月(鼠标翻了月)时,借给这个月的第一天
270
+ const tabStop = weeks.flat().includes(focused) ? focused : firstOfMonth;
271
+
272
+ // 现在画的是哪一段:起始日定了就只画它(原来那一段先让开),预览到指针或键盘所在的那天
273
+ const shown: DateRange | null =
274
+ anchor !== null ? [anchor, anchor] : committed;
275
+ const preview =
276
+ anchor !== null ? orderDates(anchor, hovered ?? focused) : null;
277
+
278
+ return (
279
+ <div
280
+ {...props}
281
+ data-month={month}
282
+ data-range={range ? "" : undefined}
283
+ data-pending={anchor !== null ? "" : undefined}
284
+ className={cn("inline-flex flex-col gap-1 text-ink", className)}
285
+ >
286
+ <div className="flex items-center justify-between gap-2">
287
+ <IconButton
288
+ size="sm"
289
+ aria-label={previousLabel}
290
+ disabled={previousDisabled}
291
+ onClick={() => turn(-1)}
292
+ >
293
+ <ChevronLeft />
294
+ </IconButton>
295
+ <div
296
+ id={titleId}
297
+ aria-live="polite"
298
+ className="text-sm font-bold tabular-nums"
299
+ >
300
+ {formats.title.format(toDateObject(firstOfMonth))}
301
+ </div>
302
+ <IconButton
303
+ size="sm"
304
+ aria-label={nextLabel}
305
+ disabled={nextDisabled}
306
+ onClick={() => turn(1)}
307
+ >
308
+ <ChevronRight />
309
+ </IconButton>
310
+ </div>
311
+
312
+ <table
313
+ role="grid"
314
+ aria-labelledby={titleId}
315
+ aria-multiselectable={range || undefined}
316
+ className="border-separate border-spacing-0"
317
+ >
318
+ <thead>
319
+ <tr>
320
+ {weeks[0]!.map((date) => (
321
+ <th
322
+ key={date}
323
+ scope="col"
324
+ abbr={formats.weekdayLong.format(toDateObject(date))}
325
+ className="h-8 w-10 text-xs font-normal text-ink-tertiary"
326
+ >
327
+ {formats.weekdayShort.format(toDateObject(date))}
328
+ </th>
329
+ ))}
330
+ </tr>
331
+ </thead>
332
+ <tbody
333
+ ref={gridRef}
334
+ onPointerLeave={() => setHovered(null)}
335
+ onKeyDown={(event) => {
336
+ // 起始日定了之后按 Esc:取消这一次,回到原来的那一段
337
+ if (event.key === "Escape" && anchor !== null) {
338
+ setAnchor(null);
339
+ return;
340
+ }
341
+ const next = moveFocus(event, focused, weekStartsOn);
342
+ if (next === null) return;
343
+ event.preventDefault();
344
+ // 键盘动了,预览跟着键盘走
345
+ setHovered(null);
346
+ focusDate(next);
347
+ }}
348
+ >
349
+ {weeks.map((week) => (
350
+ <tr key={week[0]}>
351
+ {week.map((date) => {
352
+ const isStart = shown !== null && date === shown[0];
353
+ const isEnd = shown !== null && date === shown[1];
354
+ const endpoint = isStart || isEnd;
355
+ const between =
356
+ shown !== null && date > shown[0] && date < shown[1];
357
+ const previewing =
358
+ preview !== null &&
359
+ !endpoint &&
360
+ date >= preview[0] &&
361
+ date <= preview[1];
362
+ const isToday = date === currentDay;
363
+ const outside = monthOf(date) !== month;
364
+ const disabled = unavailable(date);
365
+ const name = [
366
+ formats.day.format(toDateObject(date)),
367
+ range && isStart && startLabel,
368
+ range && isEnd && endLabel,
369
+ ]
370
+ .filter(Boolean)
371
+ .join(",");
372
+ return (
373
+ <td
374
+ key={date}
375
+ role="gridcell"
376
+ aria-selected={endpoint || between}
377
+ className="p-0"
378
+ >
379
+ <button
380
+ type="button"
381
+ data-date={date}
382
+ data-today={isToday ? "" : undefined}
383
+ data-outside={outside ? "" : undefined}
384
+ data-range-start={range && isStart ? "" : undefined}
385
+ data-range-end={range && isEnd ? "" : undefined}
386
+ data-in-range={between ? "" : undefined}
387
+ data-preview={previewing ? "" : undefined}
388
+ tabIndex={date === tabStop ? 0 : -1}
389
+ aria-label={name}
390
+ aria-current={isToday ? "date" : undefined}
391
+ // 不可选的日子仍然走得到:读屏要能读到它,只是选不了
392
+ aria-disabled={disabled || undefined}
393
+ onClick={() => select(date)}
394
+ onFocus={() => setFocused(date)}
395
+ onPointerEnter={
396
+ range ? () => setHovered(date) : undefined
397
+ }
398
+ className={cn(
399
+ "relative flex size-10 items-center justify-center font-tech text-sm tabular-nums",
400
+ "transition-colors duration-(--duration-fast) ease-standard",
401
+ focusRingInset,
402
+ endpoint
403
+ ? "bg-surface-inverse font-bold text-ink-inverse"
404
+ : [
405
+ // 一段的中间是连着的浅带;还没定结束日时是更浅的预览
406
+ between ? "bg-ink/10" : previewing && "bg-ink/5",
407
+ disabled
408
+ ? "cursor-not-allowed text-ink-disabled line-through"
409
+ : [
410
+ outside ? "text-ink-tertiary" : "text-ink",
411
+ between
412
+ ? "hover:bg-ink/15"
413
+ : "hover:bg-ink/5",
414
+ ],
415
+ ],
416
+ // 今天:数字下面一条短线,不只是换个字色
417
+ isToday &&
418
+ "after:absolute after:bottom-1.5 after:left-1/2 after:h-0.5 after:w-3 after:-translate-x-1/2 after:content-['']",
419
+ isToday &&
420
+ (endpoint
421
+ ? "after:bg-accent-ink-inverse"
422
+ : "after:bg-accent-ink"),
423
+ )}
424
+ >
425
+ {dateParts(date).day}
426
+ </button>
427
+ </td>
428
+ );
429
+ })}
430
+ </tr>
431
+ ))}
432
+ </tbody>
433
+ </table>
434
+ {range && (
435
+ // 起始日定了之后告诉读屏下一步做什么;看得见的人有那块实心的起始日和跟着走的浅带
436
+ <div aria-live="polite" className="sr-only">
437
+ {anchor !== null &&
438
+ `${formats.day.format(toDateObject(anchor))},${startLabel}。${pendingLabel}`}
439
+ </div>
440
+ )}
441
+ </div>
442
+ );
443
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ Calendar,
3
+ type CalendarProps,
4
+ type CalendarRangeProps,
5
+ type CalendarSingleProps,
6
+ type DateRange,
7
+ } from "./Calendar";