@endfield-ui/react 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +27 -0
  3. package/README.md +818 -2
  4. package/dist/index.d.ts +3445 -0
  5. package/dist/index.js +8706 -0
  6. package/dist/styles.css +2 -0
  7. package/package.json +83 -3
  8. package/src/components/README.md +179 -0
  9. package/src/components/accordion/Accordion.tsx +168 -0
  10. package/src/components/accordion/index.ts +8 -0
  11. package/src/components/alert/Alert.tsx +106 -0
  12. package/src/components/alert/index.ts +1 -0
  13. package/src/components/avatar/Avatar.tsx +92 -0
  14. package/src/components/avatar/AvatarSwitcher.tsx +261 -0
  15. package/src/components/avatar/index.ts +8 -0
  16. package/src/components/back-to-top/BackToTop.tsx +124 -0
  17. package/src/components/back-to-top/index.ts +5 -0
  18. package/src/components/badge/Badge.tsx +75 -0
  19. package/src/components/badge/index.ts +1 -0
  20. package/src/components/bracket-title/BracketTitle.tsx +40 -0
  21. package/src/components/bracket-title/index.ts +1 -0
  22. package/src/components/breadcrumb/Breadcrumb.tsx +161 -0
  23. package/src/components/breadcrumb/index.ts +6 -0
  24. package/src/components/button/Button.tsx +357 -0
  25. package/src/components/button/ButtonGroup.tsx +55 -0
  26. package/src/components/button/index.ts +13 -0
  27. package/src/components/calendar/Calendar.tsx +443 -0
  28. package/src/components/calendar/index.ts +7 -0
  29. package/src/components/carousel/Carousel.tsx +252 -0
  30. package/src/components/carousel/index.ts +7 -0
  31. package/src/components/carousel/snap-track.ts +74 -0
  32. package/src/components/checkbox/Checkbox.tsx +94 -0
  33. package/src/components/checkbox/choice-style.ts +30 -0
  34. package/src/components/checkbox/index.ts +1 -0
  35. package/src/components/chip/FilterChip.tsx +68 -0
  36. package/src/components/chip/ResourceChip.tsx +63 -0
  37. package/src/components/chip/capsule-style.ts +22 -0
  38. package/src/components/chip/index.ts +6 -0
  39. package/src/components/combobox/Combobox.tsx +450 -0
  40. package/src/components/combobox/index.ts +10 -0
  41. package/src/components/completion-banner/CompletionBanner.tsx +72 -0
  42. package/src/components/completion-banner/index.ts +4 -0
  43. package/src/components/context-menu/ContextMenu.tsx +70 -0
  44. package/src/components/context-menu/index.ts +5 -0
  45. package/src/components/corner-brackets/CornerBrackets.tsx +43 -0
  46. package/src/components/corner-brackets/index.ts +5 -0
  47. package/src/components/countdown/Countdown.tsx +145 -0
  48. package/src/components/countdown/index.ts +6 -0
  49. package/src/components/dash-indicator/DashIndicator.tsx +44 -0
  50. package/src/components/dash-indicator/index.ts +1 -0
  51. package/src/components/data-row/DataRow.tsx +282 -0
  52. package/src/components/data-row/index.ts +8 -0
  53. package/src/components/date-picker/DateField.tsx +159 -0
  54. package/src/components/date-picker/DatePicker.tsx +182 -0
  55. package/src/components/date-picker/DateRangePicker.tsx +203 -0
  56. package/src/components/date-picker/index.ts +7 -0
  57. package/src/components/dialog/Dialog.tsx +203 -0
  58. package/src/components/dialog/index.ts +7 -0
  59. package/src/components/dialog/initial-focus.ts +19 -0
  60. package/src/components/dialog/overlay-style.ts +28 -0
  61. package/src/components/drawer/Drawer.tsx +213 -0
  62. package/src/components/drawer/index.ts +6 -0
  63. package/src/components/dropdown-menu/DropdownMenu.tsx +427 -0
  64. package/src/components/dropdown-menu/MenuCheck.tsx +44 -0
  65. package/src/components/dropdown-menu/index.ts +22 -0
  66. package/src/components/dropdown-menu/menu-style.ts +47 -0
  67. package/src/components/empty-state/EmptyState.tsx +79 -0
  68. package/src/components/empty-state/index.ts +1 -0
  69. package/src/components/field/Field.tsx +203 -0
  70. package/src/components/field/index.ts +6 -0
  71. package/src/components/file-upload/FileItem.tsx +201 -0
  72. package/src/components/file-upload/FileUpload.tsx +425 -0
  73. package/src/components/file-upload/index.ts +9 -0
  74. package/src/components/flyout-bar/FlyoutBar.tsx +158 -0
  75. package/src/components/flyout-bar/index.ts +7 -0
  76. package/src/components/ghost-text/GhostText.tsx +47 -0
  77. package/src/components/ghost-text/index.ts +5 -0
  78. package/src/components/hatch/Hatch.tsx +35 -0
  79. package/src/components/hatch/index.ts +1 -0
  80. package/src/components/hazard-stripe/HazardStripe.tsx +35 -0
  81. package/src/components/hazard-stripe/index.ts +5 -0
  82. package/src/components/hover-card/HoverCard.tsx +101 -0
  83. package/src/components/hover-card/index.ts +6 -0
  84. package/src/components/icon-button/IconButton.tsx +95 -0
  85. package/src/components/icon-button/index.ts +6 -0
  86. package/src/components/image-viewer/ImageViewer.tsx +427 -0
  87. package/src/components/image-viewer/index.ts +7 -0
  88. package/src/components/input/Input.tsx +109 -0
  89. package/src/components/input/control-box.ts +71 -0
  90. package/src/components/input/index.ts +6 -0
  91. package/src/components/input/value-chip.ts +34 -0
  92. package/src/components/item-slot/ItemGrid.tsx +207 -0
  93. package/src/components/item-slot/ItemSlot.tsx +284 -0
  94. package/src/components/item-slot/index.ts +7 -0
  95. package/src/components/kbd/Kbd.tsx +33 -0
  96. package/src/components/kbd/index.ts +1 -0
  97. package/src/components/link-element/LinkElement.tsx +25 -0
  98. package/src/components/list/List.tsx +229 -0
  99. package/src/components/list/band-style.ts +18 -0
  100. package/src/components/list/index.ts +8 -0
  101. package/src/components/loader/Loader.tsx +154 -0
  102. package/src/components/loader/index.ts +1 -0
  103. package/src/components/marquee/Marquee.tsx +96 -0
  104. package/src/components/marquee/index.ts +1 -0
  105. package/src/components/media-card/MediaCard.tsx +139 -0
  106. package/src/components/media-card/index.ts +5 -0
  107. package/src/components/nav-action/NavAction.tsx +142 -0
  108. package/src/components/nav-action/index.ts +5 -0
  109. package/src/components/nav-menu/NavMenu.tsx +288 -0
  110. package/src/components/nav-menu/index.ts +6 -0
  111. package/src/components/navigator/Navigator.tsx +136 -0
  112. package/src/components/navigator/index.ts +5 -0
  113. package/src/components/otp-input/OtpInput.tsx +211 -0
  114. package/src/components/otp-input/index.ts +7 -0
  115. package/src/components/page-header/PageHeader.tsx +182 -0
  116. package/src/components/page-header/index.ts +6 -0
  117. package/src/components/pagination/Pagination.tsx +181 -0
  118. package/src/components/pagination/index.ts +5 -0
  119. package/src/components/panel/Panel.tsx +114 -0
  120. package/src/components/panel/index.ts +13 -0
  121. package/src/components/play-button/PlayButton.tsx +92 -0
  122. package/src/components/play-button/index.ts +7 -0
  123. package/src/components/popover/Popover.tsx +97 -0
  124. package/src/components/popover/index.ts +8 -0
  125. package/src/components/popover/panel-style.ts +15 -0
  126. package/src/components/progress/Progress.tsx +133 -0
  127. package/src/components/progress/ProgressRing.tsx +123 -0
  128. package/src/components/progress/index.ts +2 -0
  129. package/src/components/radio/Radio.tsx +182 -0
  130. package/src/components/radio/index.ts +7 -0
  131. package/src/components/rec-indicator/RecIndicator.tsx +47 -0
  132. package/src/components/rec-indicator/index.ts +1 -0
  133. package/src/components/registration-strip/RegistrationStrip.tsx +51 -0
  134. package/src/components/registration-strip/index.ts +5 -0
  135. package/src/components/schedule/Schedule.tsx +457 -0
  136. package/src/components/schedule/index.ts +9 -0
  137. package/src/components/scroll-area/ScrollArea.tsx +137 -0
  138. package/src/components/scroll-area/index.ts +5 -0
  139. package/src/components/scroll-hint/ScrollHint.tsx +78 -0
  140. package/src/components/scroll-hint/index.ts +5 -0
  141. package/src/components/section-title/SectionTitle.tsx +202 -0
  142. package/src/components/section-title/index.ts +5 -0
  143. package/src/components/segmented-control/SegmentedControl.tsx +220 -0
  144. package/src/components/segmented-control/index.ts +8 -0
  145. package/src/components/select/Select.tsx +390 -0
  146. package/src/components/select/index.ts +15 -0
  147. package/src/components/side-rail/SideRail.tsx +317 -0
  148. package/src/components/side-rail/SideRailSub.tsx +298 -0
  149. package/src/components/side-rail/index.ts +14 -0
  150. package/src/components/skeleton/Skeleton.tsx +70 -0
  151. package/src/components/skeleton/index.ts +1 -0
  152. package/src/components/slider/Slider.tsx +219 -0
  153. package/src/components/slider/index.ts +8 -0
  154. package/src/components/sparkline/Sparkline.tsx +122 -0
  155. package/src/components/sparkline/index.ts +6 -0
  156. package/src/components/spinner/Spinner.tsx +64 -0
  157. package/src/components/spinner/index.ts +1 -0
  158. package/src/components/stat/Stat.tsx +79 -0
  159. package/src/components/stat/index.ts +1 -0
  160. package/src/components/stepper/Stepper.tsx +316 -0
  161. package/src/components/stepper/index.ts +1 -0
  162. package/src/components/steps/Steps.tsx +302 -0
  163. package/src/components/steps/index.ts +10 -0
  164. package/src/components/switch/Switch.tsx +120 -0
  165. package/src/components/switch/index.ts +1 -0
  166. package/src/components/table/Table.tsx +560 -0
  167. package/src/components/table/index.ts +20 -0
  168. package/src/components/tabs/Tabs.tsx +313 -0
  169. package/src/components/tabs/index.ts +12 -0
  170. package/src/components/tag/Tag.tsx +66 -0
  171. package/src/components/tag/TagPair.tsx +34 -0
  172. package/src/components/tag/index.ts +2 -0
  173. package/src/components/tag-input/TagInput.tsx +502 -0
  174. package/src/components/tag-input/index.ts +8 -0
  175. package/src/components/term/Term.tsx +53 -0
  176. package/src/components/term/index.ts +1 -0
  177. package/src/components/textarea/Textarea.tsx +99 -0
  178. package/src/components/textarea/index.ts +1 -0
  179. package/src/components/texture/Texture.tsx +45 -0
  180. package/src/components/texture/index.ts +1 -0
  181. package/src/components/tick-ring/TickRing.tsx +77 -0
  182. package/src/components/tick-ring/index.ts +1 -0
  183. package/src/components/timeline/Timeline.tsx +84 -0
  184. package/src/components/timeline/index.ts +7 -0
  185. package/src/components/toast/Toast.tsx +257 -0
  186. package/src/components/toast/index.ts +10 -0
  187. package/src/components/toc/Toc.tsx +263 -0
  188. package/src/components/toc/index.ts +7 -0
  189. package/src/components/toolbar/Toolbar.tsx +389 -0
  190. package/src/components/toolbar/index.ts +17 -0
  191. package/src/components/tooltip/Tooltip.tsx +175 -0
  192. package/src/components/tooltip/index.ts +8 -0
  193. package/src/components/top-bar/TopBar.tsx +47 -0
  194. package/src/components/top-bar/index.ts +1 -0
  195. package/src/components/viewfinder/Viewfinder.tsx +97 -0
  196. package/src/components/viewfinder/index.ts +6 -0
  197. package/src/hooks/README.md +14 -0
  198. package/src/hooks/index.ts +4 -0
  199. package/src/hooks/useControllableState.ts +30 -0
  200. package/src/hooks/useInView.ts +44 -0
  201. package/src/hooks/useOverflowing.ts +28 -0
  202. package/src/hooks/usePortalScope.ts +68 -0
  203. package/src/hooks/useScrollPosition.ts +57 -0
  204. package/src/hooks/useTheme.ts +102 -0
  205. package/src/icons/ArrowCorner.tsx +7 -0
  206. package/src/icons/ArrowLeft.tsx +6 -0
  207. package/src/icons/ArrowRight.tsx +6 -0
  208. package/src/icons/CalendarIcon.tsx +10 -0
  209. package/src/icons/Check.tsx +4 -0
  210. package/src/icons/ChevronDown.tsx +3 -0
  211. package/src/icons/ChevronLeft.tsx +6 -0
  212. package/src/icons/ChevronRight.tsx +6 -0
  213. package/src/icons/Close.tsx +3 -0
  214. package/src/icons/Crosshair.tsx +7 -0
  215. package/src/icons/Lock.tsx +6 -0
  216. package/src/icons/Menu.tsx +4 -0
  217. package/src/icons/Minus.tsx +4 -0
  218. package/src/icons/Plus.tsx +3 -0
  219. package/src/icons/README.md +31 -0
  220. package/src/icons/StatusDanger.tsx +7 -0
  221. package/src/icons/StatusInfo.tsx +7 -0
  222. package/src/icons/StatusSuccess.tsx +10 -0
  223. package/src/icons/StatusWarning.tsx +7 -0
  224. package/src/icons/TriangleRight.tsx +7 -0
  225. package/src/icons/Upload.tsx +7 -0
  226. package/src/icons/createIcon.tsx +34 -0
  227. package/src/icons/index.ts +21 -0
  228. package/src/index.ts +87 -0
  229. package/src/lib/README.md +17 -0
  230. package/src/lib/cn.ts +34 -0
  231. package/src/lib/date.ts +125 -0
  232. package/src/lib/decor.ts +6 -0
  233. package/src/lib/defined.ts +9 -0
  234. package/src/lib/file.ts +61 -0
  235. package/src/lib/focus-ring.ts +11 -0
  236. package/src/lib/merge-refs.ts +11 -0
  237. package/src/styles/README.md +70 -0
  238. package/src/styles/index.css +25 -0
  239. package/src/styles/theme.css +568 -0
  240. package/src/styles/utilities.css +241 -0
@@ -0,0 +1,182 @@
1
+ import type { ReactNode, Ref } from "react";
2
+ import { useControllableState } from "../../hooks/useControllableState";
3
+ import { clampDate, today as localToday } from "../../lib/date";
4
+ import { Calendar, type CalendarProps } from "../calendar/Calendar";
5
+ import { useFieldControl } from "../field/Field";
6
+ import type { ControlSize, ControlVariant } from "../input/control-box";
7
+ import { DateField, dateFooterButton, dottedDate } from "./DateField";
8
+
9
+ export type DatePickerSize = ControlSize;
10
+ export type DatePickerVariant = ControlVariant;
11
+
12
+ export type DatePickerProps = Pick<
13
+ CalendarProps,
14
+ "min" | "max" | "isDateDisabled" | "weekStartsOn" | "locale" | "today"
15
+ > & {
16
+ /** 选中的那一天,`YYYY-MM-DD`;没选是 `null` */
17
+ value?: string | null;
18
+ defaultValue?: string | null;
19
+ /** 清除之后拿到的是 `null` */
20
+ onValueChange?: (value: string | null) => void;
21
+ /** 没选时显示的提示 */
22
+ placeholder?: ReactNode;
23
+ /** 表单字段名:选中的值(`YYYY-MM-DD`)会随表单提交 */
24
+ name?: string;
25
+ /**
26
+ * 外框的画法,和输入框相同:
27
+ * - `sunken` 凹陷的底 + 一条底边线,默认;
28
+ * - `outline` 四边描边,放在凹陷底色的区域里时用。
29
+ */
30
+ variant?: DatePickerVariant;
31
+ /** 32 / 40 / 56px 高 */
32
+ size?: DatePickerSize;
33
+ /** 日期怎么写。默认 `2026.10.09` */
34
+ format?: (date: string) => ReactNode;
35
+ /** 错误态。放在 `Field` 里时跟随它的 `error` */
36
+ invalid?: boolean;
37
+ disabled?: boolean;
38
+ /** 必填:面板里没有"清除" */
39
+ required?: boolean;
40
+ open?: boolean;
41
+ defaultOpen?: boolean;
42
+ onOpenChange?: (open: boolean) => void;
43
+ /** 面板的可访问名称 */
44
+ panelLabel?: string;
45
+ todayLabel?: string;
46
+ clearLabel?: string;
47
+ id?: string;
48
+ "aria-label"?: string;
49
+ "aria-describedby"?: string;
50
+ /** 给外框 */
51
+ className?: string;
52
+ /** 给触发按钮 */
53
+ ref?: Ref<HTMLButtonElement>;
54
+ };
55
+
56
+ /**
57
+ * 日期选择:一个字段,点开是一块带月历的面板。值是 `YYYY-MM-DD` 的字符串。
58
+ * 触发器是按钮,不能打字——要打字用原生的 `<Input type="date">`。
59
+ * 每个日期选择都要有标签:放进 `Field`,或者自己传 `aria-label`。
60
+ *
61
+ * 选一段(起止日期)用 `DateRangePicker`。
62
+ */
63
+ export function DatePicker({
64
+ value: valueProp,
65
+ defaultValue = null,
66
+ onValueChange,
67
+ placeholder = "选择日期",
68
+ name,
69
+ variant = "sunken",
70
+ size = "md",
71
+ format = dottedDate,
72
+ invalid: invalidProp,
73
+ disabled: disabledProp,
74
+ required: requiredProp,
75
+ open: openProp,
76
+ defaultOpen = false,
77
+ onOpenChange,
78
+ panelLabel = "选择日期",
79
+ todayLabel = "今天",
80
+ clearLabel = "清除",
81
+ min,
82
+ max,
83
+ isDateDisabled,
84
+ weekStartsOn,
85
+ locale,
86
+ today: todayProp,
87
+ id: idProp,
88
+ "aria-label": ariaLabel,
89
+ "aria-describedby": describedByProp,
90
+ className,
91
+ ref,
92
+ }: DatePickerProps) {
93
+ const { id, disabled, required, invalid, ...field } = useFieldControl({
94
+ id: idProp,
95
+ disabled: disabledProp,
96
+ required: requiredProp,
97
+ invalid: invalidProp,
98
+ "aria-describedby": describedByProp,
99
+ });
100
+
101
+ const [value, setValue] = useControllableState<string | null>({
102
+ value: valueProp,
103
+ defaultValue,
104
+ onChange: onValueChange,
105
+ });
106
+ const [open, setOpen] = useControllableState({
107
+ value: openProp,
108
+ defaultValue: defaultOpen,
109
+ onChange: onOpenChange,
110
+ });
111
+
112
+ const pick = (next: string | null) => {
113
+ setValue(next);
114
+ setOpen(false);
115
+ };
116
+
117
+ const todayDate = todayProp ?? localToday();
118
+ const todayUnavailable =
119
+ clampDate(todayDate, min, max) !== todayDate ||
120
+ (isDateDisabled?.(todayDate) ?? false);
121
+
122
+ return (
123
+ <DateField
124
+ ref={ref}
125
+ id={id}
126
+ display={value === null ? null : format(value)}
127
+ placeholder={placeholder}
128
+ variant={variant}
129
+ size={size}
130
+ invalid={invalid}
131
+ disabled={disabled}
132
+ required={required}
133
+ open={open}
134
+ onOpenChange={setOpen}
135
+ panelLabel={panelLabel}
136
+ aria-label={ariaLabel}
137
+ aria-describedby={field["aria-describedby"]}
138
+ className={className}
139
+ fields={
140
+ name !== undefined && (
141
+ <input
142
+ type="hidden"
143
+ name={name}
144
+ value={value ?? ""}
145
+ disabled={disabled}
146
+ />
147
+ )
148
+ }
149
+ >
150
+ <Calendar
151
+ value={value}
152
+ onValueChange={pick}
153
+ min={min}
154
+ max={max}
155
+ isDateDisabled={isDateDisabled}
156
+ weekStartsOn={weekStartsOn}
157
+ locale={locale}
158
+ today={todayProp}
159
+ />
160
+ <div className="flex items-center justify-between border-t border-line pt-2">
161
+ <button
162
+ type="button"
163
+ disabled={todayUnavailable}
164
+ onClick={() => pick(todayDate)}
165
+ className={dateFooterButton}
166
+ >
167
+ {todayLabel}
168
+ </button>
169
+ {!required && (
170
+ <button
171
+ type="button"
172
+ disabled={value === null}
173
+ onClick={() => pick(null)}
174
+ className={dateFooterButton}
175
+ >
176
+ {clearLabel}
177
+ </button>
178
+ )}
179
+ </div>
180
+ </DateField>
181
+ );
182
+ }
@@ -0,0 +1,203 @@
1
+ import type { ReactNode, Ref } from "react";
2
+ import { useControllableState } from "../../hooks/useControllableState";
3
+ import {
4
+ Calendar,
5
+ type CalendarProps,
6
+ type DateRange,
7
+ } from "../calendar/Calendar";
8
+ import { useFieldControl } from "../field/Field";
9
+ import type { ControlSize, ControlVariant } from "../input/control-box";
10
+ import { DateField, dateFooterButton, dottedDate } from "./DateField";
11
+
12
+ export type DateRangePickerProps = Pick<
13
+ CalendarProps,
14
+ "min" | "max" | "isDateDisabled" | "weekStartsOn" | "locale" | "today"
15
+ > & {
16
+ /** 选中的那一段,`[起, 止]`,两头都包含;没选是 `null` */
17
+ value?: DateRange | null;
18
+ defaultValue?: DateRange | null;
19
+ /** 一段选完才触发;清除之后拿到的是 `null` */
20
+ onValueChange?: (value: DateRange | null) => void;
21
+ /** 没选时显示的提示 */
22
+ placeholder?: ReactNode;
23
+ /** 表单字段名:起始日和结束日各一个,值是 `YYYY-MM-DD` */
24
+ startName?: string;
25
+ endName?: string;
26
+ /**
27
+ * 外框的画法,和输入框相同:
28
+ * - `sunken` 凹陷的底 + 一条底边线,默认;
29
+ * - `outline` 四边描边,放在凹陷底色的区域里时用。
30
+ */
31
+ variant?: ControlVariant;
32
+ /** 32 / 40 / 56px 高 */
33
+ size?: ControlSize;
34
+ /** 每个日期怎么写。默认 `2026.10.09` */
35
+ format?: (date: string) => ReactNode;
36
+ /** 错误态。放在 `Field` 里时跟随它的 `error` */
37
+ invalid?: boolean;
38
+ disabled?: boolean;
39
+ /** 必填:面板里没有"清除" */
40
+ required?: boolean;
41
+ open?: boolean;
42
+ defaultOpen?: boolean;
43
+ onOpenChange?: (open: boolean) => void;
44
+ /** 面板的可访问名称 */
45
+ panelLabel?: string;
46
+ /** 面板底下那句提示 */
47
+ hint?: ReactNode;
48
+ clearLabel?: string;
49
+ /** 读屏在两端那两天的名称后面听到的词 */
50
+ startLabel?: string;
51
+ endLabel?: string;
52
+ id?: string;
53
+ "aria-label"?: string;
54
+ "aria-describedby"?: string;
55
+ /** 给外框 */
56
+ className?: string;
57
+ /** 给触发按钮 */
58
+ ref?: Ref<HTMLButtonElement>;
59
+ };
60
+
61
+ /**
62
+ * 日期范围:一个字段,点开是一块带月历的面板,点两下选出起止两天。
63
+ * 值是 `[起, 止]`,两头都包含;结束日选完面板才关。
64
+ * 每个字段都要有标签:放进 `Field`,或者自己传 `aria-label`。
65
+ */
66
+ export function DateRangePicker({
67
+ value: valueProp,
68
+ defaultValue = null,
69
+ onValueChange,
70
+ placeholder = "选择起止日期",
71
+ startName,
72
+ endName,
73
+ variant = "sunken",
74
+ size = "md",
75
+ format = dottedDate,
76
+ invalid: invalidProp,
77
+ disabled: disabledProp,
78
+ required: requiredProp,
79
+ open: openProp,
80
+ defaultOpen = false,
81
+ onOpenChange,
82
+ panelLabel = "选择起止日期",
83
+ hint = "先选起始日,再选结束日",
84
+ clearLabel = "清除",
85
+ startLabel,
86
+ endLabel,
87
+ min,
88
+ max,
89
+ isDateDisabled,
90
+ weekStartsOn,
91
+ locale,
92
+ today,
93
+ id: idProp,
94
+ "aria-label": ariaLabel,
95
+ "aria-describedby": describedByProp,
96
+ className,
97
+ ref,
98
+ }: DateRangePickerProps) {
99
+ const { id, disabled, required, invalid, ...field } = useFieldControl({
100
+ id: idProp,
101
+ disabled: disabledProp,
102
+ required: requiredProp,
103
+ invalid: invalidProp,
104
+ "aria-describedby": describedByProp,
105
+ });
106
+
107
+ const [value, setValue] = useControllableState<DateRange | null>({
108
+ value: valueProp,
109
+ defaultValue,
110
+ onChange: onValueChange,
111
+ });
112
+ const [open, setOpen] = useControllableState({
113
+ value: openProp,
114
+ defaultValue: defaultOpen,
115
+ onChange: onOpenChange,
116
+ });
117
+
118
+ const pick = (next: DateRange | null) => {
119
+ setValue(next);
120
+ setOpen(false);
121
+ };
122
+
123
+ return (
124
+ <DateField
125
+ ref={ref}
126
+ id={id}
127
+ display={
128
+ value === null ? null : value[0] === value[1] ? (
129
+ // 起止是同一天:只写一个
130
+ format(value[0])
131
+ ) : (
132
+ <>
133
+ {format(value[0])}
134
+ <span aria-hidden="true" className="mx-1.5 text-ink-tertiary">
135
+ –
136
+ </span>
137
+ <span className="sr-only">至</span>
138
+ {format(value[1])}
139
+ </>
140
+ )
141
+ }
142
+ placeholder={placeholder}
143
+ variant={variant}
144
+ size={size}
145
+ invalid={invalid}
146
+ disabled={disabled}
147
+ required={required}
148
+ open={open}
149
+ onOpenChange={setOpen}
150
+ panelLabel={panelLabel}
151
+ aria-label={ariaLabel}
152
+ aria-describedby={field["aria-describedby"]}
153
+ className={className}
154
+ fields={
155
+ <>
156
+ {startName !== undefined && (
157
+ <input
158
+ type="hidden"
159
+ name={startName}
160
+ value={value?.[0] ?? ""}
161
+ disabled={disabled}
162
+ />
163
+ )}
164
+ {endName !== undefined && (
165
+ <input
166
+ type="hidden"
167
+ name={endName}
168
+ value={value?.[1] ?? ""}
169
+ disabled={disabled}
170
+ />
171
+ )}
172
+ </>
173
+ }
174
+ >
175
+ <Calendar
176
+ range
177
+ value={value}
178
+ onValueChange={pick}
179
+ min={min}
180
+ max={max}
181
+ isDateDisabled={isDateDisabled}
182
+ weekStartsOn={weekStartsOn}
183
+ locale={locale}
184
+ today={today}
185
+ startLabel={startLabel}
186
+ endLabel={endLabel}
187
+ />
188
+ <div className="flex items-center justify-between gap-3 border-t border-line pt-2">
189
+ <p className="px-2 text-xs text-ink-secondary">{hint}</p>
190
+ {!required && (
191
+ <button
192
+ type="button"
193
+ disabled={value === null}
194
+ onClick={() => pick(null)}
195
+ className={dateFooterButton}
196
+ >
197
+ {clearLabel}
198
+ </button>
199
+ )}
200
+ </div>
201
+ </DateField>
202
+ );
203
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ DatePicker,
3
+ type DatePickerProps,
4
+ type DatePickerSize,
5
+ type DatePickerVariant,
6
+ } from "./DatePicker";
7
+ export { DateRangePicker, type DateRangePickerProps } from "./DateRangePicker";
@@ -0,0 +1,203 @@
1
+ import { AlertDialog as BaseAlertDialog } from "@base-ui/react/alert-dialog";
2
+ import { Dialog as BaseDialog } from "@base-ui/react/dialog";
3
+ import {
4
+ useRef,
5
+ type ComponentProps,
6
+ type ReactElement,
7
+ type ReactNode,
8
+ } from "react";
9
+ import { usePortalScope } from "../../hooks/usePortalScope";
10
+ import { Close } from "../../icons/Close";
11
+ import { cn } from "../../lib/cn";
12
+ import { Texture } from "../texture/Texture";
13
+ import { firstFocusableIn } from "./initial-focus";
14
+ import {
15
+ overlayBackdrop,
16
+ overlayClose,
17
+ overlayHeader,
18
+ overlaySurface,
19
+ } from "./overlay-style";
20
+
21
+ export type DialogSize = "sm" | "md" | "lg";
22
+
23
+ type PopupProps = ComponentProps<typeof BaseDialog.Popup>;
24
+
25
+ export type DialogProps = Omit<
26
+ ComponentProps<"div">,
27
+ "title" | "children" | "className" | "style"
28
+ > & {
29
+ /** 触发按钮。不传就用 `open` / `onOpenChange` 自己控制 */
30
+ trigger?: ReactElement<Record<string, unknown>>;
31
+ open?: boolean;
32
+ defaultOpen?: boolean;
33
+ onOpenChange?: (open: boolean) => void;
34
+ /** 标题,在深色标题带里居中。它也是弹窗的可访问名称 */
35
+ title: ReactNode;
36
+ /** 标题下的一段说明,会关联给读屏 */
37
+ description?: ReactNode;
38
+ /** 底部的行动区:按钮靠右,主要行动在最右 */
39
+ footer?: ReactNode;
40
+ /** 最大宽度 480 / 640 / 1040px,窄屏上通宽。默认 `md` */
41
+ size?: DialogSize;
42
+ /**
43
+ * 需要用户明确回应的确认(破坏性操作):角色是 `alertdialog`,点遮罩不关。
44
+ * `Esc` 和关闭图标仍然可用。
45
+ */
46
+ alert?: boolean;
47
+ /** 标题下方加一排小方点。纯装饰 */
48
+ ornament?: boolean;
49
+ /**
50
+ * 左下角的线稿,纯装饰。本库不带线稿:传自己的原创图形,
51
+ * 用 `currentColor` 画会跟着取低对比的墨色。高 48px
52
+ */
53
+ cornerArt?: ReactNode;
54
+ /** 关闭图标的可访问名称 */
55
+ closeLabel?: string;
56
+ /** 打开时焦点落在哪。默认是内容区里第一个可聚焦的元素:第一个字段,或者行动区的第一个按钮 */
57
+ initialFocus?: PopupProps["initialFocus"];
58
+ /** 关闭后焦点回到哪。默认是触发按钮 */
59
+ finalFocus?: PopupProps["finalFocus"];
60
+ /** 给弹窗本身 */
61
+ className?: string;
62
+ children?: ReactNode;
63
+ };
64
+
65
+ const ORNAMENT_DOTS = [0, 1, 2, 3, 4, 5];
66
+
67
+ const sizeClass: Record<DialogSize, string> = {
68
+ sm: "max-w-[30rem]",
69
+ md: "max-w-[40rem]",
70
+ lg: "max-w-[65rem]",
71
+ };
72
+
73
+ /**
74
+ * 弹窗:深色斜纹标题带 + 点阵底的内容区。打开时焦点移入并被限制在内,
75
+ * 关闭后回到触发按钮;背景不可滚动、不可交互。不要弹窗套弹窗。
76
+ */
77
+ export function Dialog({
78
+ trigger,
79
+ open,
80
+ defaultOpen,
81
+ onOpenChange,
82
+ title,
83
+ description,
84
+ footer,
85
+ size = "md",
86
+ alert = false,
87
+ ornament = false,
88
+ cornerArt,
89
+ closeLabel = "关闭",
90
+ initialFocus,
91
+ finalFocus,
92
+ className,
93
+ children,
94
+ ...props
95
+ }: DialogProps) {
96
+ const { anchorRef, portalRef } = usePortalScope();
97
+ const contentRef = useRef<HTMLDivElement>(null);
98
+ // 两者除了 Root 与 Trigger 是同一批部件;确认弹窗的 Root 把角色和"点遮罩不关"定死了
99
+ const Base = (alert ? BaseAlertDialog : BaseDialog) as typeof BaseDialog;
100
+
101
+ return (
102
+ <Base.Root
103
+ open={open}
104
+ defaultOpen={defaultOpen}
105
+ onOpenChange={onOpenChange && ((next) => onOpenChange(next))}
106
+ >
107
+ {trigger && <Base.Trigger ref={anchorRef} render={trigger} />}
108
+ <Base.Portal ref={portalRef}>
109
+ <Base.Backdrop className={overlayBackdrop} />
110
+ <Base.Viewport className="fixed inset-0 z-(--z-overlay) flex items-center justify-center overflow-hidden p-4">
111
+ <Base.Popup
112
+ {...props}
113
+ initialFocus={initialFocus ?? firstFocusableIn(contentRef)}
114
+ finalFocus={finalFocus}
115
+ data-size={size}
116
+ className={cn(
117
+ overlaySurface,
118
+ "relative flex max-h-full min-h-0 w-full flex-col",
119
+ "transition-[scale,opacity] duration-(--duration-base) ease-exit",
120
+ "data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0",
121
+ sizeClass[size],
122
+ className,
123
+ )}
124
+ >
125
+ <div
126
+ data-theme="dark"
127
+ className={cn(overlayHeader, "min-h-16 justify-center px-12")}
128
+ >
129
+ <div className="flex flex-col items-center gap-1.5 py-3">
130
+ {/* 全站的标题大多是粗体,弹窗标题是例外:深底白字本身已经够醒目 */}
131
+ <Base.Title className="text-center text-xl font-normal wrap-anywhere">
132
+ {title}
133
+ </Base.Title>
134
+ {ornament && (
135
+ <span
136
+ aria-hidden="true"
137
+ data-ornament=""
138
+ className="flex gap-1"
139
+ >
140
+ {ORNAMENT_DOTS.map((dot) => (
141
+ <span key={dot} className="size-1 bg-ink/50" />
142
+ ))}
143
+ </span>
144
+ )}
145
+ </div>
146
+ <Base.Close aria-label={closeLabel} className={overlayClose}>
147
+ <Close size={24} />
148
+ </Base.Close>
149
+ </div>
150
+
151
+ <div
152
+ ref={contentRef}
153
+ className="relative flex min-h-0 flex-auto flex-col"
154
+ >
155
+ <Texture />
156
+ {/* 正文垫一层实色底,点阵只在四周露出一圈;内容过长时只有这一块滚动 */}
157
+ <div className="relative min-h-0 flex-auto overflow-y-auto overscroll-contain p-4">
158
+ <div className="bg-surface-raised p-5 wrap-anywhere">
159
+ {description && (
160
+ <Base.Description
161
+ className={cn(
162
+ "text-ink-secondary",
163
+ children !== undefined && "mb-4",
164
+ )}
165
+ >
166
+ {description}
167
+ </Base.Description>
168
+ )}
169
+ {children}
170
+ </div>
171
+ {cornerArt && (
172
+ // 排在正文那块实色底的下面、压在点阵上:不压正文,跟着内容一起滚
173
+ <div
174
+ aria-hidden="true"
175
+ data-corner-art=""
176
+ className="pointer-events-none mt-3 flex h-12 items-end text-ink-tertiary [&_svg]:h-full [&_svg]:w-auto"
177
+ >
178
+ {cornerArt}
179
+ </div>
180
+ )}
181
+ </div>
182
+ {footer && (
183
+ <div className="relative flex shrink-0 flex-wrap justify-end gap-3 border-t border-line bg-surface-raised px-5 py-4">
184
+ {footer}
185
+ </div>
186
+ )}
187
+ </div>
188
+ </Base.Popup>
189
+ </Base.Viewport>
190
+ </Base.Portal>
191
+ </Base.Root>
192
+ );
193
+ }
194
+
195
+ export type DialogCloseProps = {
196
+ /** 点了就关掉弹窗的按钮,比如"取消" */
197
+ children: ReactElement<Record<string, unknown>>;
198
+ };
199
+
200
+ /** 包住行动区里"点了就关"的按钮。弹窗和抽屉里都能用。 */
201
+ export function DialogClose({ children }: DialogCloseProps) {
202
+ return <BaseDialog.Close render={children} />;
203
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ Dialog,
3
+ DialogClose,
4
+ type DialogCloseProps,
5
+ type DialogProps,
6
+ type DialogSize,
7
+ } from "./Dialog";
@@ -0,0 +1,19 @@
1
+ import type { RefObject } from "react";
2
+
3
+ const FOCUSABLE = [
4
+ "a[href]",
5
+ "button:not([disabled])",
6
+ 'input:not([disabled]):not([type="hidden"])',
7
+ "select:not([disabled])",
8
+ "textarea:not([disabled])",
9
+ '[tabindex]:not([tabindex="-1"])',
10
+ ].join(",");
11
+
12
+ /**
13
+ * 弹窗与抽屉打开时焦点落在哪:内容区里第一个能聚焦的元素——表单的第一个字段,
14
+ * 或者行动区的第一个按钮(约定是"取消",破坏性确认因此不会一回车就执行)。
15
+ * 内容区里没有时返回 `true`,交还给默认行为(标题带里的关闭按钮)。
16
+ */
17
+ export function firstFocusableIn(content: RefObject<HTMLElement | null>) {
18
+ return () => content.current?.querySelector<HTMLElement>(FOCUSABLE) ?? true;
19
+ }
@@ -0,0 +1,28 @@
1
+ import { focusRingInset } from "../../lib/focus-ring";
2
+
3
+ /*
4
+ * 弹窗与抽屉共用的外观:遮罩、深色标题带、关闭按钮。
5
+ * 标题带在亮、暗两个主题下都是深色,所以它自己是一个 `data-theme="dark"` 的
6
+ * 局部主题:里面的文字、关闭图标、焦点环都按深色底取值,不用逐个写死。
7
+ */
8
+
9
+ /** 遮罩:纯黑半透明(亮 50% / 暗 70%),不模糊、不带色彩 */
10
+ export const overlayBackdrop =
11
+ "fixed inset-0 z-(--z-overlay) bg-scrim transition-opacity duration-(--duration-fast) ease-standard data-ending-style:opacity-0 data-starting-style:opacity-0";
12
+
13
+ /** 标题带的底:深色 + 斜纹。用的时候在元素上加 `data-theme="dark"` */
14
+ export const overlayHeader =
15
+ "hatch relative flex shrink-0 items-center border-b border-line bg-surface-raised text-ink";
16
+
17
+ /** 关闭按钮:24px 的叉、40px 的点击区,悬停转 90° */
18
+ export const overlayClose = [
19
+ "absolute top-1/2 right-1 inline-flex size-10 -translate-y-1/2 items-center justify-center text-ink",
20
+ // 按下的浅灰是官网实测值的近似,只出现在深色带上
21
+ "active:text-neutral-300",
22
+ "[&_svg]:transition-transform [&_svg]:duration-(--duration-base) [&_svg]:ease-standard hover:[&_svg]:rotate-90",
23
+ focusRingInset,
24
+ ].join(" ");
25
+
26
+ /** 浮层的面:直角、中性底、环境阴影。高对比模式下阴影不显示,补一圈边线 */
27
+ export const overlaySurface =
28
+ "bg-surface-raised text-ink shadow-lg outline-none forced-colors:border";