@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,102 @@
1
+ import { useEffect, useSyncExternalStore } from "react";
2
+
3
+ export type Theme = "light" | "dark";
4
+ export type ThemePreference = Theme | "system";
5
+
6
+ type Snapshot = { theme: ThemePreference; resolvedTheme: Theme };
7
+
8
+ const STORAGE_KEY = "ef-theme";
9
+ const DARK_QUERY = "(prefers-color-scheme: dark)";
10
+
11
+ const serverSnapshot: Snapshot = { theme: "light", resolvedTheme: "light" };
12
+ const listeners = new Set<() => void>();
13
+ let snapshot: Snapshot | null = null;
14
+ let stopWatchingSystem: (() => void) | null = null;
15
+
16
+ function isPreference(value: unknown): value is ThemePreference {
17
+ return value === "light" || value === "dark" || value === "system";
18
+ }
19
+
20
+ function resolve(theme: ThemePreference): Theme {
21
+ if (theme !== "system") return theme;
22
+ return window.matchMedia?.(DARK_QUERY).matches ? "dark" : "light";
23
+ }
24
+
25
+ /** 取值顺序:上次保存的选择 → `<html data-theme>` 现有的值 → 亮色 */
26
+ function readPreference(): ThemePreference {
27
+ try {
28
+ const stored = window.localStorage.getItem(STORAGE_KEY);
29
+ if (isPreference(stored)) return stored;
30
+ } catch {
31
+ // 存储不可用(隐私模式等)时退回到下面的来源
32
+ }
33
+ const attribute = document.documentElement.dataset.theme;
34
+ return attribute === "dark" ? "dark" : "light";
35
+ }
36
+
37
+ function getSnapshot(): Snapshot {
38
+ if (!snapshot) {
39
+ const theme = readPreference();
40
+ snapshot = { theme, resolvedTheme: resolve(theme) };
41
+ }
42
+ return snapshot;
43
+ }
44
+
45
+ function commit(theme: ThemePreference) {
46
+ snapshot = { theme, resolvedTheme: resolve(theme) };
47
+ document.documentElement.dataset.theme = snapshot.resolvedTheme;
48
+ for (const listener of listeners) listener();
49
+ }
50
+
51
+ function subscribe(listener: () => void) {
52
+ listeners.add(listener);
53
+
54
+ if (!stopWatchingSystem) {
55
+ const media = window.matchMedia?.(DARK_QUERY);
56
+ const onChange = () => {
57
+ if (getSnapshot().theme === "system") commit("system");
58
+ };
59
+ media?.addEventListener("change", onChange);
60
+ stopWatchingSystem = () => media?.removeEventListener("change", onChange);
61
+ }
62
+
63
+ return () => {
64
+ listeners.delete(listener);
65
+ if (listeners.size === 0) {
66
+ stopWatchingSystem?.();
67
+ stopWatchingSystem = null;
68
+ }
69
+ };
70
+ }
71
+
72
+ function setTheme(theme: ThemePreference) {
73
+ try {
74
+ window.localStorage.setItem(STORAGE_KEY, theme);
75
+ } catch {
76
+ // 存不下来也照常切换,只是刷新后不保留
77
+ }
78
+ commit(theme);
79
+ }
80
+
81
+ /**
82
+ * 读写 `<html data-theme>`。
83
+ *
84
+ * - `theme`:用户的选择,可以是 `"system"`;
85
+ * - `resolvedTheme`:实际生效的 `"light"` 或 `"dark"`;
86
+ * - `setTheme`:切换并记到 `localStorage`。
87
+ */
88
+ export function useTheme(): Snapshot & {
89
+ setTheme: (theme: ThemePreference) => void;
90
+ } {
91
+ const { theme, resolvedTheme } = useSyncExternalStore(
92
+ subscribe,
93
+ getSnapshot,
94
+ () => serverSnapshot,
95
+ );
96
+
97
+ useEffect(() => {
98
+ document.documentElement.dataset.theme = resolvedTheme;
99
+ }, [resolvedTheme]);
100
+
101
+ return { theme, resolvedTheme, setTheme };
102
+ }
@@ -0,0 +1,7 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ /** 斜箭头 ↘:分节引导、"查看详情" */
4
+ export const ArrowCorner = createIcon(
5
+ "ArrowCorner",
6
+ <path d="M5 5l14 14M19 7v12H7" />,
7
+ );
@@ -0,0 +1,6 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ export const ArrowLeft = createIcon(
4
+ "ArrowLeft",
5
+ <path d="M20 12H5M11 5l-7 7 7 7" />,
6
+ );
@@ -0,0 +1,6 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ export const ArrowRight = createIcon(
4
+ "ArrowRight",
5
+ <path d="M4 12h15M13 5l7 7-7 7" />,
6
+ );
@@ -0,0 +1,10 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ /**
4
+ * 日历:日期字段右侧的记号。
5
+ * 名字带 `Icon` 是因为 `Calendar` 已经是月历控件了
6
+ */
7
+ export const CalendarIcon = createIcon(
8
+ "CalendarIcon",
9
+ <path d="M4 6h16v14H4zM4 11h16M8 3v5M16 3v5" />,
10
+ );
@@ -0,0 +1,4 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ /** 对勾:已选、已完成 */
4
+ export const Check = createIcon("Check", <path d="M4 12l6 6L20 6" />);
@@ -0,0 +1,3 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ export const ChevronDown = createIcon("ChevronDown", <path d="M4 9l8 8 8-8" />);
@@ -0,0 +1,6 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ export const ChevronLeft = createIcon(
4
+ "ChevronLeft",
5
+ <path d="M15 4l-8 8 8 8" />,
6
+ );
@@ -0,0 +1,6 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ export const ChevronRight = createIcon(
4
+ "ChevronRight",
5
+ <path d="M9 4l8 8-8 8" />,
6
+ );
@@ -0,0 +1,3 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ export const Close = createIcon("Close", <path d="M5 5l14 14M19 5L5 19" />);
@@ -0,0 +1,7 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ /** 四个取景角加中心的准星:图鉴里"未获得"的占位符 */
4
+ export const Crosshair = createIcon(
5
+ "Crosshair",
6
+ <path d="M3 8V3h5M16 3h5v5M21 16v5h-5M8 21H3v-5M12 9v6M9 12h6" />,
7
+ );
@@ -0,0 +1,6 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ export const Lock = createIcon(
4
+ "Lock",
5
+ <path d="M5 11h14v10H5zM8 11V7a4 4 0 0 1 8 0v4" />,
6
+ );
@@ -0,0 +1,4 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ /** 三条横线:打开菜单 */
4
+ export const Menu = createIcon("Menu", <path d="M3 6h18M3 12h18M3 18h18" />);
@@ -0,0 +1,4 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ /** 短横:半选、减少 */
4
+ export const Minus = createIcon("Minus", <path d="M4 12h16" />);
@@ -0,0 +1,3 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ export const Plus = createIcon("Plus", <path d="M12 4v16M4 12h16" />);
@@ -0,0 +1,31 @@
1
+ # icons
2
+
3
+ 原创图标组件。目前只有控件自己用到的二十个:
4
+
5
+ | 图标 | 用在哪 |
6
+ | --- | --- |
7
+ | `ArrowCorner`(斜箭头 ↘)、`ArrowLeft`、`ArrowRight` | 分节标题、文字按钮 |
8
+ | `ChevronLeft`、`ChevronRight`、`ChevronDown` | 返回按钮、翻页、滚动提示 |
9
+ | `TriangleRight`(实心三角 ▶) | 页签的箭头块 |
10
+ | `Close`、`Plus`、`Minus`、`Check` | 关闭、增减、已选与半选 |
11
+ | `StatusInfo`、`StatusSuccess`、`StatusWarning`、`StatusDanger` | 提示条的四种色调、输入框的错误图标 |
12
+ | `Lock`、`Crosshair`(四个取景角加准星) | 物品格的"锁定"与"未获得" |
13
+ | `Menu`(三条横线) | 顶栏的菜单钮 |
14
+ | `CalendarIcon` | 日期选择右侧的记号(`Calendar` 这个名字给了月历控件) |
15
+ | `Upload`(托盘加一支朝上的箭头) | 文件上传的投放区 |
16
+
17
+ 四个状态图标的外形各不相同——方框、圆、三角、菱形——状态因此不只靠颜色区分。
18
+
19
+ ## 规则
20
+
21
+ - **只收录原创图标。** 不收录、不描摹官方图标与 Logo,包括游戏内图标、官网导航图标和阵营徽记。
22
+ - 绘制规范见 [图标规范](../../../../docs/design/foundations/iconography.md):24 网格、2px 描边、平头端点、尖角拐角、几何母题(菱形、三角、斜杠)。
23
+ - 每个图标一个文件,用 [`createIcon`](createIcon.tsx) 生成:接受 `size` 与 `className`,颜色用 `currentColor`,默认 `aria-hidden`。
24
+
25
+ ```tsx
26
+ import { createIcon } from "./createIcon";
27
+
28
+ export const Plus = createIcon("Plus", <path d="M12 4v16M4 12h16" />);
29
+ ```
30
+
31
+ - 通用图标(关闭、箭头、搜索等)优先画一套自己的;需要大量通用图标时,可以在文档站里搭配成熟的开源图标库,但不要打包进本库。
@@ -0,0 +1,7 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ /** 菱形:危险、错误 */
4
+ export const StatusDanger = createIcon(
5
+ "StatusDanger",
6
+ <path d="M12 2.5l9.5 9.5-9.5 9.5L2.5 12zM12 7v6M12 15v2" />,
7
+ );
@@ -0,0 +1,7 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ /** 方框:说明。四种状态图标各用一种外形,不只靠颜色区分 */
4
+ export const StatusInfo = createIcon(
5
+ "StatusInfo",
6
+ <path d="M4 4h16v16H4zM12 11v6M12 7v2" />,
7
+ );
@@ -0,0 +1,10 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ /** 圆 + 对勾:成功 */
4
+ export const StatusSuccess = createIcon(
5
+ "StatusSuccess",
6
+ <>
7
+ <circle cx="12" cy="12" r="9" />
8
+ <path d="M7.5 12.5l3 3 6-7" />
9
+ </>,
10
+ );
@@ -0,0 +1,7 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ /** 三角:警告 */
4
+ export const StatusWarning = createIcon(
5
+ "StatusWarning",
6
+ <path d="M12 4l9 16H3zM12 10v5M12 16.5v2" />,
7
+ );
@@ -0,0 +1,7 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ /** 实心三角 ▶:前往、展开 */
4
+ export const TriangleRight = createIcon(
5
+ "TriangleRight",
6
+ <path d="M7 4l12 8-12 8z" fill="currentColor" stroke="none" />,
7
+ );
@@ -0,0 +1,7 @@
1
+ import { createIcon } from "./createIcon";
2
+
3
+ /** 上传:一个托盘,上面一支朝上的箭头 */
4
+ export const Upload = createIcon(
5
+ "Upload",
6
+ <path d="M12 16V4M7 9l5-5 5 5M4 15v5h16v-5" />,
7
+ );
@@ -0,0 +1,34 @@
1
+ import type { ComponentProps, ReactNode } from "react";
2
+
3
+ export type IconProps = Omit<ComponentProps<"svg">, "children"> & {
4
+ /** 显示尺寸,默认 24。线宽随尺寸等比缩放 */
5
+ size?: number | string;
6
+ };
7
+
8
+ /**
9
+ * 按图标规范生成组件:24 网格、2px 描边、平头端点、尖角拐角、颜色继承。
10
+ * 默认对辅助技术隐藏;单独承担含义时由使用方传 `aria-label` 并去掉 `aria-hidden`。
11
+ */
12
+ export function createIcon(displayName: string, content: ReactNode) {
13
+ function Icon({ size = 24, ...props }: IconProps) {
14
+ return (
15
+ <svg
16
+ width={size}
17
+ height={size}
18
+ viewBox="0 0 24 24"
19
+ fill="none"
20
+ stroke="currentColor"
21
+ strokeWidth={2}
22
+ strokeLinecap="butt"
23
+ strokeLinejoin="miter"
24
+ aria-hidden="true"
25
+ focusable="false"
26
+ {...props}
27
+ >
28
+ {content}
29
+ </svg>
30
+ );
31
+ }
32
+ Icon.displayName = displayName;
33
+ return Icon;
34
+ }
@@ -0,0 +1,21 @@
1
+ export { createIcon, type IconProps } from "./createIcon";
2
+ export { ArrowCorner } from "./ArrowCorner";
3
+ export { ArrowLeft } from "./ArrowLeft";
4
+ export { ArrowRight } from "./ArrowRight";
5
+ export { CalendarIcon } from "./CalendarIcon";
6
+ export { Check } from "./Check";
7
+ export { ChevronDown } from "./ChevronDown";
8
+ export { ChevronLeft } from "./ChevronLeft";
9
+ export { ChevronRight } from "./ChevronRight";
10
+ export { Close } from "./Close";
11
+ export { Crosshair } from "./Crosshair";
12
+ export { Lock } from "./Lock";
13
+ export { Menu } from "./Menu";
14
+ export { Minus } from "./Minus";
15
+ export { Plus } from "./Plus";
16
+ export { StatusDanger } from "./StatusDanger";
17
+ export { StatusInfo } from "./StatusInfo";
18
+ export { StatusSuccess } from "./StatusSuccess";
19
+ export { StatusWarning } from "./StatusWarning";
20
+ export { TriangleRight } from "./TriangleRight";
21
+ export { Upload } from "./Upload";
package/src/index.ts ADDED
@@ -0,0 +1,82 @@
1
+ export * from "./components/accordion";
2
+ export * from "./components/alert";
3
+ export * from "./components/avatar";
4
+ export * from "./components/back-to-top";
5
+ export * from "./components/badge";
6
+ export * from "./components/bracket-title";
7
+ export * from "./components/breadcrumb";
8
+ export * from "./components/button";
9
+ export * from "./components/calendar";
10
+ export * from "./components/carousel";
11
+ export * from "./components/checkbox";
12
+ export * from "./components/chip";
13
+ export * from "./components/combobox";
14
+ export * from "./components/completion-banner";
15
+ export * from "./components/context-menu";
16
+ export * from "./components/corner-brackets";
17
+ export * from "./components/countdown";
18
+ export * from "./components/data-row";
19
+ export * from "./components/date-picker";
20
+ export * from "./components/dash-indicator";
21
+ export * from "./components/dialog";
22
+ export * from "./components/drawer";
23
+ export * from "./components/dropdown-menu";
24
+ export * from "./components/empty-state";
25
+ export * from "./components/field";
26
+ export * from "./components/file-upload";
27
+ export * from "./components/flyout-bar";
28
+ export * from "./components/ghost-text";
29
+ export * from "./components/hatch";
30
+ export * from "./components/hazard-stripe";
31
+ export * from "./components/icon-button";
32
+ export * from "./components/input";
33
+ export * from "./components/item-slot";
34
+ export * from "./components/kbd";
35
+ export * from "./components/list";
36
+ export * from "./components/loader";
37
+ export * from "./components/marquee";
38
+ export * from "./components/media-card";
39
+ export * from "./components/nav-action";
40
+ export * from "./components/nav-menu";
41
+ export * from "./components/navigator";
42
+ export * from "./components/pagination";
43
+ export * from "./components/panel";
44
+ export * from "./components/play-button";
45
+ export * from "./components/popover";
46
+ export * from "./components/progress";
47
+ export * from "./components/radio";
48
+ export * from "./components/rec-indicator";
49
+ export * from "./components/registration-strip";
50
+ export * from "./components/schedule";
51
+ export * from "./components/scroll-hint";
52
+ export * from "./components/section-title";
53
+ export * from "./components/segmented-control";
54
+ export * from "./components/select";
55
+ export * from "./components/side-rail";
56
+ export * from "./components/skeleton";
57
+ export * from "./components/slider";
58
+ export * from "./components/sparkline";
59
+ export * from "./components/spinner";
60
+ export * from "./components/stat";
61
+ export * from "./components/stepper";
62
+ export * from "./components/steps";
63
+ export * from "./components/switch";
64
+ export * from "./components/table";
65
+ export * from "./components/tabs";
66
+ export * from "./components/tag";
67
+ export * from "./components/tag-input";
68
+ export * from "./components/term";
69
+ export * from "./components/textarea";
70
+ export * from "./components/texture";
71
+ export * from "./components/tick-ring";
72
+ export * from "./components/timeline";
73
+ export * from "./components/toast";
74
+ export * from "./components/toc";
75
+ export * from "./components/toolbar";
76
+ export * from "./components/tooltip";
77
+ export * from "./components/top-bar";
78
+ export * from "./components/viewfinder";
79
+
80
+ export * from "./hooks";
81
+ export * from "./icons";
82
+ export { cn } from "./lib/cn";
@@ -0,0 +1,17 @@
1
+ # lib
2
+
3
+ 纯函数与常量。这里不放 React 组件和 hooks。
4
+
5
+ | 文件 | 内容 |
6
+ | --- | --- |
7
+ | `cn.ts` | `cn()`:合并类名(`clsx` + `tailwind-merge`)。里面登记了 [theme.css](../styles/theme.css) 的自定义令牌名,否则 `text-micro` 会被当成颜色、与 `text-ink` 互相覆盖 |
8
+ | `focus-ring.ts` | `focusRing`、`focusRingInset`、`focusRingWithin`:全库统一的焦点环类名 |
9
+ | `merge-refs.ts` | `mergeRefs()`:组件自己要用节点、同时还得把它交给使用方时,把几个 ref 合成一个 |
10
+ | `defined.ts` | `defined()`:去掉值是 `undefined` 的属性。把控件的属性合并到使用方给的元素上(`render`)之前用,免得把那个元素自己写的盖掉 |
11
+ | `decor.ts` | `decor`:装饰层共用的类 |
12
+ | `date.ts` | 日期的纯函数:加减日和月、比较、把两天排成先后、夹到范围内、月历的六行七列。日期一律是 `YYYY-MM-DD` 的字符串,全部按 UTC 算,不碰本地时区。`Calendar`、`DatePicker`、`Schedule` 共用,不对外导出 |
13
+ | `file.ts` | 文件的纯函数:按 `accept` 认类型(扩展名、MIME、`image/*` 通配)、把字节数写成 `1.2 MB`、取扩展名、判断是不是同一个文件。`FileUpload` 用,不对外导出 |
14
+
15
+ 没有单独的"变体工具":各组件直接写 `Record<Variant, string>` 再用 `cn()` 合并,够用且类型清楚。
16
+
17
+ 以后可能放进来的:与 DOM 无关的格式化函数,例如把数字格式化成等宽的 `01 / 04`。
package/src/lib/cn.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { extendTailwindMerge } from "tailwind-merge";
3
+
4
+ /*
5
+ * theme.css 里的自定义令牌名需要登记,否则 tailwind-merge 会把 `text-micro`
6
+ * 当成颜色,与 `text-ink` 互相覆盖。改动 theme.css 的令牌名时同步改这里。
7
+ */
8
+ const twMerge = extendTailwindMerge({
9
+ extend: {
10
+ theme: {
11
+ text: ["micro", "ghost"],
12
+ tracking: ["ghost", "label"],
13
+ shadow: ["rail"],
14
+ ease: ["standard", "emphasis", "exit"],
15
+ animate: [
16
+ "flicker",
17
+ "fill",
18
+ "scroll-hint",
19
+ "breathe",
20
+ "marquee",
21
+ "spin-slow",
22
+ "blink",
23
+ "slide-in",
24
+ "turn-in",
25
+ "fade-in",
26
+ "indeterminate",
27
+ ],
28
+ },
29
+ },
30
+ });
31
+
32
+ export function cn(...inputs: ClassValue[]): string {
33
+ return twMerge(clsx(inputs));
34
+ }
@@ -0,0 +1,125 @@
1
+ /*
2
+ * 日期的纯函数。日期一律是 `YYYY-MM-DD` 的字符串:没有时间,也没有时区
3
+ * (和原生 `<input type="date">` 的值是同一种写法)。月份是 `YYYY-MM`。
4
+ *
5
+ * 全部按 UTC 算:本地时区和夏令时会让"加一天"偶尔变成加 23 或 25 小时,
6
+ * 落到前一天或后一天上。只有"今天是几号"要问本地时间。
7
+ */
8
+
9
+ const DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
10
+ const MONTH = /^(\d{4})-(\d{2})$/;
11
+ const DAY_MS = 86_400_000;
12
+
13
+ const pad = (value: number, length = 2) => String(value).padStart(length, "0");
14
+
15
+ const toUTC = (date: string) => {
16
+ const match = DATE.exec(date);
17
+ if (!match) return Number.NaN;
18
+ return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
19
+ };
20
+
21
+ const fromUTC = (time: number) => {
22
+ const date = new Date(time);
23
+ return `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
24
+ };
25
+
26
+ /** 是不是一个真的日期:格式对,而且这一天存在(`2026-02-30` 不算) */
27
+ export function isDate(value: unknown): value is string {
28
+ return typeof value === "string" && fromUTC(toUTC(value)) === value;
29
+ }
30
+
31
+ /** 今天,按本地时间 */
32
+ export function today(): string {
33
+ const now = new Date();
34
+ return `${pad(now.getFullYear(), 4)}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
35
+ }
36
+
37
+ /** 拆成年、月(1 – 12)、日 */
38
+ export function dateParts(date: string) {
39
+ const [year, month, day] = date.split("-").map(Number);
40
+ return { year: year!, month: month!, day: day! };
41
+ }
42
+
43
+ export function addDays(date: string, days: number): string {
44
+ return fromUTC(toUTC(date) + days * DAY_MS);
45
+ }
46
+
47
+ /** 这个月有几天 */
48
+ export function daysInMonth(year: number, month: number): number {
49
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
50
+ }
51
+
52
+ /** 加减月份。那个月没有这一天就落在月末:1 月 31 日加一个月是 2 月 28 日 */
53
+ export function addMonths(date: string, months: number): string {
54
+ const { year, month, day } = dateParts(date);
55
+ const index = year * 12 + (month - 1) + months;
56
+ const nextYear = Math.floor(index / 12);
57
+ const nextMonth = (index % 12) + 1;
58
+ return `${pad(nextYear, 4)}-${pad(nextMonth)}-${pad(Math.min(day, daysInMonth(nextYear, nextMonth)))}`;
59
+ }
60
+
61
+ /** 从 `from` 到 `to` 隔了几天;`to` 在前时是负数 */
62
+ export function daysBetween(from: string, to: string): number {
63
+ return Math.round((toUTC(to) - toUTC(from)) / DAY_MS);
64
+ }
65
+
66
+ /** 星期几:0 是星期日,6 是星期六 */
67
+ export function dayOfWeek(date: string): number {
68
+ return new Date(toUTC(date)).getUTCDay();
69
+ }
70
+
71
+ /** 夹到 `min` 和 `max` 之间。同一种写法的日期可以直接按字符串比大小 */
72
+ export function clampDate(date: string, min?: string, max?: string): string {
73
+ if (min && date < min) return min;
74
+ if (max && date > max) return max;
75
+ return date;
76
+ }
77
+
78
+ /** 把两天排成先后:`[早的, 晚的]`。同一天就是两个一样的 */
79
+ export function orderDates(a: string, b: string): [string, string] {
80
+ return a <= b ? [a, b] : [b, a];
81
+ }
82
+
83
+ /** 这个日期所在的月份,`YYYY-MM` */
84
+ export function monthOf(date: string): string {
85
+ return date.slice(0, 7);
86
+ }
87
+
88
+ export function isMonth(value: unknown): value is string {
89
+ if (typeof value !== "string") return false;
90
+ const match = MONTH.exec(value);
91
+ return !!match && Number(match[2]) >= 1 && Number(match[2]) <= 12;
92
+ }
93
+
94
+ export function addMonthsToMonth(month: string, months: number): string {
95
+ return monthOf(addMonths(`${month}-01`, months));
96
+ }
97
+
98
+ /** 这一周的第一天。`weekStartsOn`:0 是从星期日起,1 是从星期一起 */
99
+ export function startOfWeek(date: string, weekStartsOn: 0 | 1 = 1): string {
100
+ return addDays(date, -((dayOfWeek(date) - weekStartsOn + 7) % 7));
101
+ }
102
+
103
+ /**
104
+ * 一个月的月历:固定六行七列,前后用相邻月份的日子补齐。
105
+ * 固定六行是为了翻月时高度不跳
106
+ */
107
+ export function monthGrid(month: string, weekStartsOn: 0 | 1 = 1): string[][] {
108
+ const first = startOfWeek(`${month}-01`, weekStartsOn);
109
+ return Array.from({ length: 6 }, (_, week) =>
110
+ Array.from({ length: 7 }, (_, day) => addDays(first, week * 7 + day)),
111
+ );
112
+ }
113
+
114
+ /** 从 `start` 到 `end`(都包含)的每一天 */
115
+ export function eachDay(start: string, end: string): string[] {
116
+ const count = daysBetween(start, end) + 1;
117
+ return Array.from({ length: Math.max(0, count) }, (_, offset) =>
118
+ addDays(start, offset),
119
+ );
120
+ }
121
+
122
+ /** 交给 `Intl.DateTimeFormat` 用的 `Date`。格式化时要带 `timeZone: "UTC"` */
123
+ export function toDateObject(date: string): Date {
124
+ return new Date(toUTC(date));
125
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 装饰层共用的类:不挡操作、不可选中;高对比模式与打印时整个去掉。
3
+ * 用它的元素同时要加 `aria-hidden="true"`。
4
+ */
5
+ export const decor =
6
+ "pointer-events-none select-none forced-colors:hidden print:hidden";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 去掉值是 `undefined` 的属性。把控件的属性合并到使用方给的元素上之前用:
3
+ * 控件没有的东西(比如 `href`),不能把那个元素自己写的盖掉。
4
+ */
5
+ export function defined<T extends object>(props: T): Partial<T> {
6
+ return Object.fromEntries(
7
+ Object.entries(props).filter(([, value]) => value !== undefined),
8
+ ) as Partial<T>;
9
+ }