@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,357 @@
1
+ import type { ComponentProps, MouseEvent, ReactNode } from "react";
2
+ import { ArrowCorner } from "../../icons/ArrowCorner";
3
+ import { ChevronLeft } from "../../icons/ChevronLeft";
4
+ import { cn } from "../../lib/cn";
5
+ import { focusRing } from "../../lib/focus-ring";
6
+ import { LinkElement, type LinkRender } from "../link-element/LinkElement";
7
+ import { Spinner } from "../spinner/Spinner";
8
+
9
+ export type ButtonVariant =
10
+ "control" | "action" | "light" | "back" | "text" | "danger";
11
+ export type ButtonSize = "sm" | "md" | "lg";
12
+
13
+ type OwnProps = {
14
+ /**
15
+ * - `control` 深灰底 + 黄色竖条,默认;
16
+ * - `action` 信号黄底,一屏只用一个;
17
+ * - `light` 白底,用在深色底上或作次要项;
18
+ * - `back` 深灰斜纹底 + 左箭头,返回上一级;
19
+ * - `text` 无底、下划线、斜箭头,低强调;
20
+ * - `danger` 同 `control`,竖条为红色,用于不可逆操作。
21
+ */
22
+ variant?: ButtonVariant;
23
+ size?: ButtonSize;
24
+ /** 文字左侧的图标 */
25
+ iconStart?: ReactNode;
26
+ /** 文字右侧的图标;`text` 变体默认是斜箭头,传 `null` 去掉 */
27
+ iconEnd?: ReactNode;
28
+ /** 提交中:记号变成旋转指示,尺寸不变,不响应点击 */
29
+ loading?: boolean;
30
+ /** 提交中显示的文字,如"处理中"。传了之后按钮按两段文字里较宽的一段定宽 */
31
+ loadingText?: ReactNode;
32
+ /** 撑满容器宽度 */
33
+ block?: boolean;
34
+ disabled?: boolean;
35
+ };
36
+
37
+ type ButtonAsButton = OwnProps &
38
+ Omit<ComponentProps<"button">, keyof OwnProps> & {
39
+ href?: undefined;
40
+ render?: undefined;
41
+ };
42
+ type ButtonAsLink = OwnProps &
43
+ Omit<ComponentProps<"a">, keyof OwnProps> & {
44
+ /** 传了就渲染成链接 */
45
+ href: string;
46
+ render?: LinkRender;
47
+ };
48
+ type ButtonAsRendered = OwnProps &
49
+ Omit<ComponentProps<"a">, keyof OwnProps> & {
50
+ href?: undefined;
51
+ /**
52
+ * 用这个元素代替 `<a>`,给路由库的链接组件用:
53
+ * `<Button render={<Link to="/archive" />}>`。传了就按链接处理
54
+ */
55
+ render: LinkRender;
56
+ };
57
+
58
+ export type ButtonProps = ButtonAsButton | ButtonAsLink | ButtonAsRendered;
59
+
60
+ /** 左侧带竖条的变体:竖条绝对定位,不参与文字居中 */
61
+ const markerColor: Partial<Record<ButtonVariant, string>> = {
62
+ control: "bg-action",
63
+ light: "bg-neutral-500",
64
+ danger: "bg-alert",
65
+ };
66
+
67
+ const sizeClass: Record<ButtonSize, string> = {
68
+ sm: "h-8 gap-1.5 text-sm",
69
+ md: "h-10 gap-2 text-base",
70
+ lg: "h-14 gap-2.5 text-lg",
71
+ };
72
+
73
+ /*
74
+ * 带竖条的变体左右留白对称,并且比规格表的"水平内边距"更宽:
75
+ * 要给"箭头盒 + 悬停位移"留出位置,文字再长也不会被箭头压到。
76
+ */
77
+ const markerPadding: Record<ButtonSize, string> = {
78
+ sm: "min-w-16 px-7",
79
+ md: "min-w-24 px-10",
80
+ lg: "min-w-40 px-13",
81
+ };
82
+
83
+ const markerBox: Record<ButtonSize, string> = {
84
+ sm: "left-3 h-4 w-2 [--marker-bar:2px]",
85
+ md: "left-4 h-5 w-2.5",
86
+ lg: "left-6 h-7 w-3",
87
+ };
88
+
89
+ const markerShift: Record<ButtonSize, string> = {
90
+ sm: "group-hover:translate-x-1 group-active:translate-x-1",
91
+ md: "group-hover:translate-x-1.5 group-active:translate-x-1.5",
92
+ lg: "group-hover:translate-x-2 group-active:translate-x-2",
93
+ };
94
+
95
+ const markerSpinner: Record<ButtonSize, string> = {
96
+ sm: "left-3 size-2",
97
+ md: "left-4 size-2.5",
98
+ lg: "left-6 size-3",
99
+ };
100
+
101
+ /* action:文字在分隔线左侧的区域里居中,分隔线右侧留一窄条 */
102
+ const actionPadding: Record<ButtonSize, string> = {
103
+ sm: "min-w-16 pl-4 pr-8",
104
+ md: "min-w-24 pl-6 pr-11",
105
+ lg: "min-w-40 pl-8 pr-15",
106
+ };
107
+
108
+ const actionDivider: Record<ButtonSize, string> = {
109
+ sm: "right-3.5 w-0.5",
110
+ md: "right-4.5 w-[3px]",
111
+ lg: "right-6 w-[3px]",
112
+ };
113
+
114
+ const actionSpinner: Record<ButtonSize, string> = {
115
+ sm: "right-[3px] size-2",
116
+ md: "right-[5px] size-2",
117
+ lg: "right-1.5 size-3",
118
+ };
119
+
120
+ const backPadding: Record<ButtonSize, string> = {
121
+ sm: "min-w-16 px-3.5",
122
+ md: "min-w-24 px-5",
123
+ lg: "min-w-40 px-7",
124
+ };
125
+
126
+ const inlineIconSize: Record<ButtonSize, number> = { sm: 14, md: 16, lg: 20 };
127
+
128
+ const enabledClass: Record<ButtonVariant, string> = {
129
+ control:
130
+ "bg-control text-on-control shadow-xs hover:bg-control-hover hover:text-white active:bg-control-pressed active:text-white",
131
+ danger:
132
+ "bg-control text-on-control shadow-xs hover:bg-control-hover hover:text-white active:bg-control-pressed active:text-white",
133
+ // 有意不随主题翻转:它本来就是给深色底用的
134
+ light:
135
+ "bg-neutral-0 text-neutral-900 shadow-xs hover:bg-neutral-100 active:bg-neutral-200",
136
+ // 黄色按钮悬停时底色不变,只有分隔线加深
137
+ action: "bg-action text-on-action shadow-md active:bg-action-pressed",
138
+ back: "hatch hatch-mid bg-control text-white shadow-xs hover:bg-neutral-600 active:bg-control-pressed",
139
+ text: "text-ink active:text-ink-secondary",
140
+ };
141
+
142
+ export function Button(props: ButtonProps) {
143
+ const {
144
+ variant = "control",
145
+ size = "md",
146
+ iconStart,
147
+ iconEnd,
148
+ loading = false,
149
+ loadingText,
150
+ block = false,
151
+ disabled = false,
152
+ className,
153
+ children,
154
+ ...rest
155
+ } = props;
156
+
157
+ const inert = disabled || loading;
158
+ const isText = variant === "text";
159
+ const marker = markerColor[variant];
160
+ const iconSize = inlineIconSize[size];
161
+
162
+ const classes = cn(
163
+ "group relative inline-flex shrink-0 items-center justify-center font-medium leading-none whitespace-nowrap select-none",
164
+ "transition-colors duration-(--duration-fast) ease-standard",
165
+ focusRing,
166
+ sizeClass[size],
167
+ marker && markerPadding[size],
168
+ variant === "action" && actionPadding[size],
169
+ variant === "back" && backPadding[size],
170
+ isText && "h-auto border-b border-current py-1",
171
+ disabled
172
+ ? isText
173
+ ? "cursor-not-allowed text-ink-disabled"
174
+ : "cursor-not-allowed bg-disabled text-on-disabled"
175
+ : enabledClass[variant],
176
+ loading && "pointer-events-none",
177
+ block && "flex w-full",
178
+ className,
179
+ );
180
+
181
+ // 每个变体自己的记号在加载时变成旋转指示,所以按钮的宽度不会变
182
+ let start = iconStart;
183
+ let end = iconEnd;
184
+ if (variant === "back" && start === undefined) {
185
+ start = loading ? (
186
+ <span
187
+ className="inline-flex items-center justify-center"
188
+ style={{ width: iconSize, height: iconSize }}
189
+ >
190
+ <Spinner label={null} className="size-2" />
191
+ </span>
192
+ ) : (
193
+ <ChevronLeft size={iconSize} />
194
+ );
195
+ }
196
+ if (isText && end === undefined) {
197
+ end = loading ? (
198
+ <span
199
+ className="inline-flex items-center justify-center"
200
+ style={{ width: iconSize, height: iconSize }}
201
+ >
202
+ <Spinner label={null} className="size-2" />
203
+ </span>
204
+ ) : (
205
+ <ArrowCorner
206
+ size={iconSize}
207
+ className={cn(
208
+ "transition-[translate] duration-(--duration-base) ease-standard",
209
+ !disabled &&
210
+ "group-hover:translate-x-0.5 group-hover:translate-y-0.5",
211
+ )}
212
+ />
213
+ );
214
+ }
215
+
216
+ const label =
217
+ loadingText === undefined ? (
218
+ children
219
+ ) : (
220
+ // 两段文字叠在同一格里,按较宽的一段定宽;不显示的那段同时对读屏隐藏
221
+ <span className="grid justify-items-center">
222
+ <span
223
+ aria-hidden={loading || undefined}
224
+ className={cn("col-start-1 row-start-1", loading && "invisible")}
225
+ >
226
+ {children}
227
+ </span>
228
+ <span
229
+ aria-hidden={!loading || undefined}
230
+ className={cn("col-start-1 row-start-1", !loading && "invisible")}
231
+ >
232
+ {loadingText}
233
+ </span>
234
+ </span>
235
+ );
236
+
237
+ const content = (
238
+ <>
239
+ {marker &&
240
+ (loading ? (
241
+ <span
242
+ className={cn(
243
+ "pointer-events-none absolute top-1/2 -translate-y-1/2",
244
+ markerSpinner[size],
245
+ )}
246
+ >
247
+ <Spinner label={null} className={cn("block size-full", marker)} />
248
+ </span>
249
+ ) : (
250
+ <span
251
+ aria-hidden="true"
252
+ className={cn(
253
+ "marker-bar pointer-events-none absolute top-1/2 -translate-y-1/2",
254
+ "transition-[clip-path,translate] duration-(--duration-base) ease-standard",
255
+ markerBox[size],
256
+ disabled
257
+ ? "bg-on-disabled"
258
+ : [
259
+ marker,
260
+ "group-hover:marker-arrow group-active:marker-arrow",
261
+ markerShift[size],
262
+ ],
263
+ )}
264
+ />
265
+ ))}
266
+ {start}
267
+ {label}
268
+ {end}
269
+ {variant === "action" && (
270
+ <>
271
+ <span
272
+ aria-hidden="true"
273
+ className={cn(
274
+ "pointer-events-none absolute top-[17.5%] h-[65%]",
275
+ "transition-colors duration-(--duration-fast) ease-standard",
276
+ actionDivider[size],
277
+ disabled
278
+ ? "bg-on-disabled/20"
279
+ : "bg-on-action/10 group-hover:bg-on-action/15 group-active:bg-on-action/20",
280
+ )}
281
+ />
282
+ {loading && (
283
+ <span
284
+ className={cn(
285
+ "pointer-events-none absolute top-1/2 -translate-y-1/2",
286
+ actionSpinner[size],
287
+ )}
288
+ >
289
+ <Spinner label={null} className="block size-full" />
290
+ </span>
291
+ )}
292
+ </>
293
+ )}
294
+ </>
295
+ );
296
+
297
+ const shared = {
298
+ className: classes,
299
+ "data-variant": variant,
300
+ "data-size": size,
301
+ "data-loading": loading ? "" : undefined,
302
+ "aria-busy": loading || undefined,
303
+ };
304
+
305
+ if (rest.href !== undefined || rest.render !== undefined) {
306
+ const { href, render, onClick, ...anchorProps } = rest;
307
+ const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
308
+ if (inert) {
309
+ event.preventDefault();
310
+ return;
311
+ }
312
+ onClick?.(event);
313
+ };
314
+ return (
315
+ <LinkElement
316
+ {...anchorProps}
317
+ {...shared}
318
+ render={render}
319
+ // 禁用的链接去掉 href,保留 link 角色,让读屏仍能读出"不可用"
320
+ href={inert ? undefined : href}
321
+ role={inert ? "link" : anchorProps.role}
322
+ aria-disabled={inert || undefined}
323
+ onClick={handleClick}
324
+ >
325
+ {content}
326
+ </LinkElement>
327
+ );
328
+ }
329
+
330
+ const {
331
+ href: _href,
332
+ render: _render,
333
+ onClick,
334
+ type = "button",
335
+ ...buttonProps
336
+ } = rest;
337
+ const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
338
+ if (inert) {
339
+ event.preventDefault();
340
+ return;
341
+ }
342
+ onClick?.(event);
343
+ };
344
+ return (
345
+ <button
346
+ {...buttonProps}
347
+ {...shared}
348
+ type={type}
349
+ disabled={disabled}
350
+ // 加载中不用原生 disabled:保住焦点,读屏还能读到"忙碌"
351
+ aria-disabled={loading || undefined}
352
+ onClick={handleClick}
353
+ >
354
+ {content}
355
+ </button>
356
+ );
357
+ }
@@ -0,0 +1,55 @@
1
+ import type { ComponentProps } from "react";
2
+ import { cn } from "../../lib/cn";
3
+
4
+ export type ButtonGroupAlign = "start" | "end" | "between";
5
+ export type ButtonGroupOrientation = "horizontal" | "vertical";
6
+ export type ButtonGroupGap = "sm" | "md";
7
+
8
+ export type ButtonGroupProps = ComponentProps<"div"> & {
9
+ /** 横排时靠哪一边。默认 `end`:主要行动放在最右 */
10
+ align?: ButtonGroupAlign;
11
+ /** 竖排时按钮等宽,主要行动放在最下 */
12
+ orientation?: ButtonGroupOrientation;
13
+ /** 按钮之间 8px 或 12px */
14
+ gap?: ButtonGroupGap;
15
+ };
16
+
17
+ const alignClass: Record<ButtonGroupAlign, string> = {
18
+ start: "justify-start",
19
+ end: "justify-end",
20
+ between: "justify-between",
21
+ };
22
+
23
+ const gapClass: Record<ButtonGroupGap, string> = {
24
+ sm: "gap-2",
25
+ md: "gap-3",
26
+ };
27
+
28
+ /**
29
+ * 并排的一组按钮。主要行动写在最后(最右或最下),一组里最多一个 `action`;
30
+ * "取消 + 确认"用 `light` + `control`,把黄色留给真正重要的时刻。
31
+ * 放不下时整组换行,不挤压按钮。
32
+ */
33
+ export function ButtonGroup({
34
+ align = "end",
35
+ orientation = "horizontal",
36
+ gap = "md",
37
+ className,
38
+ ...props
39
+ }: ButtonGroupProps) {
40
+ return (
41
+ <div
42
+ role="group"
43
+ {...props}
44
+ data-orientation={orientation}
45
+ className={cn(
46
+ "flex",
47
+ gapClass[gap],
48
+ orientation === "vertical"
49
+ ? "flex-col items-stretch"
50
+ : ["flex-wrap items-center", alignClass[align]],
51
+ className,
52
+ )}
53
+ />
54
+ );
55
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ Button,
3
+ type ButtonProps,
4
+ type ButtonSize,
5
+ type ButtonVariant,
6
+ } from "./Button";
7
+ export {
8
+ ButtonGroup,
9
+ type ButtonGroupAlign,
10
+ type ButtonGroupGap,
11
+ type ButtonGroupOrientation,
12
+ type ButtonGroupProps,
13
+ } from "./ButtonGroup";