@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,7 @@
1
+ export {
2
+ Carousel,
3
+ CarouselSlide,
4
+ type CarouselProps,
5
+ type CarouselRatio,
6
+ type CarouselSlideProps,
7
+ } from "./Carousel";
@@ -0,0 +1,74 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+
3
+ /** 滚动停下之后多久算"停稳了"。没有 scrollend 事件的浏览器靠它 */
4
+ const SETTLE_MS = 120;
5
+
6
+ type SnapTrackOptions = {
7
+ /** 现在是第几张,从 0 起 */
8
+ index: number;
9
+ /** 一共几张 */
10
+ count: number;
11
+ /** 用户自己滑、停稳在了第几张 */
12
+ onSettle: (index: number) => void;
13
+ };
14
+
15
+ /**
16
+ * 一条横向滚动加滚动吸附的轨道,每一张和轨道一样宽。下标和滚动位置互相跟:
17
+ * 下标变了轨道滚过去;用户自己滑的时候反过来,等滚动停稳,由停下的位置定下标。
18
+ * 手势、惯性都是浏览器的。媒体轮播和图片查看共用,不对外导出。
19
+ *
20
+ * 把 `trackRef` 和 `onScroll` 交给那个滚动容器。
21
+ */
22
+ export function useSnapTrack({ index, count, onSettle }: SnapTrackOptions) {
23
+ const trackRef = useRef<HTMLDivElement>(null);
24
+ const settleTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
25
+ const last = Math.max(0, count - 1);
26
+ // 给不跟着渲染重建的回调(尺寸变化)读最新的下标
27
+ const indexRef = useRef(index);
28
+ indexRef.current = index;
29
+ const placed = useRef(false);
30
+
31
+ // 下标变了:滚过去。已经在那儿(用户自己滑过去的)就不动。
32
+ // 刚挂上的那一次直接到位:从第一张一路滑到第三张不是谁想看的
33
+ useEffect(() => {
34
+ const first = !placed.current;
35
+ placed.current = true;
36
+ const track = trackRef.current;
37
+ if (!track || track.clientWidth === 0) return;
38
+ if (Math.round(track.scrollLeft / track.clientWidth) === index) return;
39
+ track.scrollTo?.({
40
+ left: index * track.clientWidth,
41
+ behavior: first ? "instant" : "smooth",
42
+ });
43
+ }, [index]);
44
+
45
+ // 宽度变了:轨道的滚动位置是像素,会错开,重新对到当前这一张(不要动画)
46
+ useEffect(() => {
47
+ const track = trackRef.current;
48
+ if (!track || typeof ResizeObserver === "undefined") return;
49
+ const observer = new ResizeObserver(() => {
50
+ track.scrollTo?.({
51
+ left: indexRef.current * track.clientWidth,
52
+ behavior: "instant",
53
+ });
54
+ });
55
+ observer.observe(track);
56
+ return () => observer.disconnect();
57
+ }, []);
58
+
59
+ useEffect(() => () => clearTimeout(settleTimer.current), []);
60
+
61
+ // 用户自己滑:等滚动停稳,由停下的位置定下标。滚动途中不改——
62
+ // 翻页钮触发的滚动会路过中间那几张
63
+ const onScroll = useCallback(() => {
64
+ clearTimeout(settleTimer.current);
65
+ settleTimer.current = setTimeout(() => {
66
+ const track = trackRef.current;
67
+ if (!track || track.clientWidth === 0) return;
68
+ const stopped = Math.round(track.scrollLeft / track.clientWidth);
69
+ onSettle(Math.min(Math.max(stopped, 0), last));
70
+ }, SETTLE_MS);
71
+ }, [onSettle, last]);
72
+
73
+ return { trackRef, onScroll };
74
+ }
@@ -0,0 +1,94 @@
1
+ import {
2
+ useEffect,
3
+ useRef,
4
+ type ChangeEvent,
5
+ type ComponentProps,
6
+ type ReactNode,
7
+ } from "react";
8
+ import { Check } from "../../icons/Check";
9
+ import { Minus } from "../../icons/Minus";
10
+ import { cn } from "../../lib/cn";
11
+ import { focusRing } from "../../lib/focus-ring";
12
+ import { mergeRefs } from "../../lib/merge-refs";
13
+ import {
14
+ choiceBox,
15
+ choiceControl,
16
+ choiceFrame,
17
+ choiceLabel,
18
+ choiceMark,
19
+ } from "./choice-style";
20
+
21
+ export type CheckboxProps = Omit<ComponentProps<"input">, "type" | "size"> & {
22
+ /** 半选:一组子项只选了一部分。显示为短横,读屏读作"部分选中" */
23
+ indeterminate?: boolean;
24
+ /** 错误态:描边变红 */
25
+ invalid?: boolean;
26
+ onCheckedChange?: (checked: boolean) => void;
27
+ /** 右侧的文字标签。不传时必须自己给 `aria-label` */
28
+ children?: ReactNode;
29
+ };
30
+
31
+ /**
32
+ * 复选框。原生 `<input type="checkbox">`,受控用 `checked`、非受控用 `defaultChecked`。
33
+ * `className` 给整行,其余属性与 `ref` 给 `<input>`。
34
+ */
35
+ export function Checkbox({
36
+ indeterminate = false,
37
+ invalid = false,
38
+ onCheckedChange,
39
+ onChange,
40
+ className,
41
+ children,
42
+ ref,
43
+ ...props
44
+ }: CheckboxProps) {
45
+ const inputRef = useRef<HTMLInputElement>(null);
46
+
47
+ // indeterminate 只能通过 DOM 属性设置,没有对应的 HTML 特性
48
+ useEffect(() => {
49
+ if (inputRef.current) inputRef.current.indeterminate = indeterminate;
50
+ }, [indeterminate]);
51
+
52
+ const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
53
+ onChange?.(event);
54
+ onCheckedChange?.(event.target.checked);
55
+ };
56
+
57
+ return (
58
+ <label className={cn(choiceLabel, className)}>
59
+ <span className={choiceControl}>
60
+ <span className={choiceFrame}>
61
+ <input
62
+ {...props}
63
+ ref={mergeRefs(inputRef, ref)}
64
+ type="checkbox"
65
+ aria-invalid={invalid || undefined}
66
+ onChange={handleChange}
67
+ className={cn(
68
+ choiceBox,
69
+ // 半选只属于复选框:一组单选都没选时也会命中 :indeterminate,
70
+ // 所以这两个类不能放进与单选共用的 choiceBox
71
+ "indeterminate:border-surface-inverse indeterminate:bg-surface-inverse",
72
+ focusRing,
73
+ )}
74
+ />
75
+ <Check
76
+ size={14}
77
+ strokeWidth={3}
78
+ className={cn(
79
+ choiceMark,
80
+ // 菱形方案下"已选"就是实心菱形,不画对勾
81
+ "peer-checked:opacity-100 peer-indeterminate:opacity-0 choice-diamond:hidden",
82
+ )}
83
+ />
84
+ <Minus
85
+ size={14}
86
+ strokeWidth={3}
87
+ className={cn(choiceMark, "peer-indeterminate:opacity-100")}
88
+ />
89
+ </span>
90
+ </span>
91
+ {children !== undefined && <span className="min-w-0">{children}</span>}
92
+ </label>
93
+ );
94
+ }
@@ -0,0 +1,30 @@
1
+ /*
2
+ * 复选、单选、开关共用的一行:真正的 <input> 画成控件本身,文字标签在右,
3
+ * 点击标签等同点击控件,整行不低于 40px。
4
+ */
5
+
6
+ export const choiceLabel =
7
+ "group inline-flex min-h-10 items-start gap-2 py-2 text-base text-ink has-disabled:cursor-not-allowed has-disabled:text-ink-disabled";
8
+
9
+ /** 控件的定位盒:高度等于一行文字,标签换行时控件仍然对齐第一行 */
10
+ export const choiceControl = "flex h-[1lh] shrink-0 items-center";
11
+
12
+ /*
13
+ * 复选框与单选框的方格 / 圆:
14
+ * 未选是凹陷底 + 2px 描边;已选是反转块,上面压 accent-ink-inverse 的记号——
15
+ * 亮色下是墨底黄记号,暗色下反转块是近白,记号换成深色档。
16
+ *
17
+ * 祖先上有 data-choice="diamond" 时换成菱形(方案 B):方格缩到 14px 并转 45°,
18
+ * 对角线正好还是 20px;未选是空心菱形,已选是实心的,不再画对勾和圆点——
19
+ * 所以禁用的"已选"也要保持实心,否则和"未选"分不出来。
20
+ */
21
+ export const choiceBox =
22
+ "peer size-5 shrink-0 appearance-none border-2 choice-diamond:size-3.5 choice-diamond:rotate-45 choice-diamond:checked:disabled:bg-ink-disabled border-line-strong bg-surface-sunken transition-colors duration-(--duration-fast) ease-standard group-hover:border-ink-secondary checked:border-surface-inverse checked:bg-surface-inverse aria-invalid:border-danger disabled:cursor-not-allowed disabled:border-ink-disabled disabled:bg-surface-sunken";
23
+
24
+ /** 方格 / 圆的定位盒:菱形比它小一圈,所以居中 */
25
+ export const choiceFrame =
26
+ "relative inline-flex size-5 items-center justify-center";
27
+
28
+ /** 压在方格 / 圆正中的记号,默认透明,由 peer 的状态点亮 */
29
+ export const choiceMark =
30
+ "pointer-events-none absolute inset-0 m-auto text-accent-ink-inverse opacity-0 peer-disabled:text-ink-disabled";
@@ -0,0 +1 @@
1
+ export { Checkbox, type CheckboxProps } from "./Checkbox";
@@ -0,0 +1,68 @@
1
+ import type { ComponentProps, MouseEvent } from "react";
2
+ import { useControllableState } from "../../hooks/useControllableState";
3
+ import { cn } from "../../lib/cn";
4
+ import { focusRing } from "../../lib/focus-ring";
5
+ import {
6
+ capsuleBase,
7
+ capsuleSize,
8
+ capsuleState,
9
+ type CapsuleSize,
10
+ } from "./capsule-style";
11
+
12
+ export type FilterChipProps = Omit<ComponentProps<"button">, "aria-pressed"> & {
13
+ /** 受控的选中状态 */
14
+ selected?: boolean;
15
+ /** 非受控时的初始状态 */
16
+ defaultSelected?: boolean;
17
+ onSelectedChange?: (selected: boolean) => void;
18
+ size?: CapsuleSize;
19
+ };
20
+
21
+ /**
22
+ * 筛选胶囊:一个可以独立开关的条件,几个并排就是多选筛选。
23
+ * 几项里只能选一项时用 `Tabs` 的 `capsule` 变体或单选。
24
+ */
25
+ export function FilterChip({
26
+ selected,
27
+ defaultSelected = false,
28
+ onSelectedChange,
29
+ size = "md",
30
+ disabled = false,
31
+ type = "button",
32
+ className,
33
+ onClick,
34
+ ...props
35
+ }: FilterChipProps) {
36
+ const [on, setOn] = useControllableState({
37
+ value: selected,
38
+ defaultValue: defaultSelected,
39
+ onChange: onSelectedChange,
40
+ });
41
+
42
+ return (
43
+ <button
44
+ {...props}
45
+ type={type}
46
+ disabled={disabled}
47
+ aria-pressed={on}
48
+ data-selected={on ? "" : undefined}
49
+ onClick={(event: MouseEvent<HTMLButtonElement>) => {
50
+ onClick?.(event);
51
+ if (!event.defaultPrevented) setOn(!on);
52
+ }}
53
+ className={cn(
54
+ capsuleBase,
55
+ focusRing,
56
+ capsuleSize[size],
57
+ disabled
58
+ ? on
59
+ ? capsuleState.selectedDisabled
60
+ : capsuleState.disabled
61
+ : on
62
+ ? capsuleState.selected
63
+ : capsuleState.rest,
64
+ className,
65
+ )}
66
+ />
67
+ );
68
+ }
@@ -0,0 +1,63 @@
1
+ import type { ComponentProps, ReactNode } from "react";
2
+ import { cn } from "../../lib/cn";
3
+
4
+ export type ResourceChipSize = "sm" | "md";
5
+
6
+ export type ResourceChipProps = Omit<ComponentProps<"span">, "children"> & {
7
+ /** 资源的图标,放进左端的圆形底托。本库不带资源图标,由使用方提供 */
8
+ icon?: ReactNode;
9
+ /** 数量,等宽数字 */
10
+ children: ReactNode;
11
+ /** 给读屏的完整说明,如"燃料 1,280"。提供后数字本身对辅助技术隐藏 */
12
+ label?: string;
13
+ size?: ResourceChipSize;
14
+ };
15
+
16
+ const sizeClass: Record<ResourceChipSize, string> = {
17
+ sm: "h-6 gap-1 pr-2.5 text-xs",
18
+ md: "h-7 gap-1.5 pr-3 text-sm",
19
+ };
20
+
21
+ const iconPadding: Record<ResourceChipSize, string> = {
22
+ sm: "pl-0.5",
23
+ md: "pl-1",
24
+ };
25
+
26
+ const plainPadding: Record<ResourceChipSize, string> = {
27
+ sm: "pl-2.5",
28
+ md: "pl-3",
29
+ };
30
+
31
+ /** 资源胶囊:货币、材料的当前数量——会变的状态,所以是胶囊。 */
32
+ export function ResourceChip({
33
+ icon,
34
+ label,
35
+ size = "md",
36
+ className,
37
+ children,
38
+ ...props
39
+ }: ResourceChipProps) {
40
+ return (
41
+ <span
42
+ {...props}
43
+ className={cn(
44
+ // 有意不随主题翻转:control 系列在两个主题下取值相同
45
+ "inline-flex shrink-0 items-center rounded-full bg-control font-tech leading-none font-bold whitespace-nowrap text-on-control tabular-nums",
46
+ sizeClass[size],
47
+ icon ? iconPadding[size] : plainPadding[size],
48
+ className,
49
+ )}
50
+ >
51
+ {icon && (
52
+ <span
53
+ aria-hidden="true"
54
+ className="inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-on-control/15 [&_svg]:size-3"
55
+ >
56
+ {icon}
57
+ </span>
58
+ )}
59
+ <span aria-hidden={label ? "true" : undefined}>{children}</span>
60
+ {label && <span className="sr-only">{label}</span>}
61
+ </span>
62
+ );
63
+ }
@@ -0,0 +1,22 @@
1
+ /*
2
+ * 可选中的胶囊:细边,选中时反转为墨底。
3
+ * 筛选胶囊与页签的 `capsule` 变体是同一个外观,类名放在一处免得两边走样。
4
+ */
5
+
6
+ export type CapsuleSize = "sm" | "md";
7
+
8
+ export const capsuleBase =
9
+ "inline-flex shrink-0 items-center rounded-full border leading-none font-medium whitespace-nowrap transition-colors duration-(--duration-fast) ease-standard";
10
+
11
+ export const capsuleSize: Record<CapsuleSize, string> = {
12
+ sm: "h-7 px-3 text-xs",
13
+ md: "h-8 px-4 text-sm",
14
+ };
15
+
16
+ export const capsuleState = {
17
+ selected: "border-transparent bg-surface-inverse text-ink-inverse",
18
+ rest: "border-line-strong text-ink hover:bg-ink/5",
19
+ disabled: "cursor-not-allowed border-line text-ink-disabled",
20
+ selectedDisabled:
21
+ "cursor-not-allowed border-transparent bg-disabled text-on-disabled",
22
+ } as const;
@@ -0,0 +1,6 @@
1
+ export { FilterChip, type FilterChipProps } from "./FilterChip";
2
+ export {
3
+ ResourceChip,
4
+ type ResourceChipProps,
5
+ type ResourceChipSize,
6
+ } from "./ResourceChip";