@endfield-ui/react 0.1.0 → 0.3.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 (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +55 -5
  3. package/dist/index.d.ts +213 -4
  4. package/dist/index.js +688 -109
  5. package/dist/styles.css +1 -1
  6. package/package.json +1 -1
  7. package/src/components/README.md +8 -1
  8. package/src/components/avatar/Avatar.tsx +3 -0
  9. package/src/components/carousel/Carousel.tsx +7 -47
  10. package/src/components/carousel/snap-track.ts +74 -0
  11. package/src/components/checkbox/choice-style.ts +5 -2
  12. package/src/components/corner-brackets/CornerBrackets.tsx +2 -1
  13. package/src/components/dialog/Dialog.tsx +4 -2
  14. package/src/components/dropdown-menu/menu-style.ts +9 -2
  15. package/src/components/file-upload/FileUpload.tsx +1 -1
  16. package/src/components/hover-card/HoverCard.tsx +101 -0
  17. package/src/components/hover-card/index.ts +6 -0
  18. package/src/components/image-viewer/ImageViewer.tsx +467 -0
  19. package/src/components/image-viewer/index.ts +7 -0
  20. package/src/components/item-slot/ItemSlot.tsx +3 -1
  21. package/src/components/nav-menu/NavMenu.tsx +14 -1
  22. package/src/components/otp-input/OtpInput.tsx +214 -0
  23. package/src/components/otp-input/index.ts +7 -0
  24. package/src/components/page-header/PageHeader.tsx +182 -0
  25. package/src/components/page-header/index.ts +6 -0
  26. package/src/components/popover/Popover.tsx +4 -12
  27. package/src/components/popover/panel-style.ts +15 -0
  28. package/src/components/scroll-area/ScrollArea.tsx +137 -0
  29. package/src/components/scroll-area/index.ts +5 -0
  30. package/src/components/slider/Slider.tsx +6 -2
  31. package/src/components/steps/Steps.tsx +12 -3
  32. package/src/components/table/Table.tsx +1 -0
  33. package/src/components/tag-input/TagInput.tsx +1 -0
  34. package/src/components/toast/Toast.tsx +18 -5
  35. package/src/components/toc/Toc.tsx +1 -0
  36. package/src/components/tooltip/Tooltip.tsx +5 -1
  37. package/src/components/viewfinder/Viewfinder.tsx +9 -0
  38. package/src/index.ts +5 -0
  39. package/src/lib/README.md +1 -0
  40. package/src/lib/cn.ts +2 -0
  41. package/src/lib/motion.ts +15 -0
  42. package/src/styles/theme.css +43 -0
  43. package/src/styles/utilities.css +2 -1
@@ -287,6 +287,7 @@ export function TagInput({
287
287
  onKeyDown?.(event);
288
288
  if (event.defaultPrevented || readOnly) return;
289
289
  // 输入法正在组字时的回车是在选字,不是提交
290
+ // oxlint-disable-next-line typescript/no-deprecated -- Safari 上确认选字的那一下回车,isComposing 已经是 false,只有 keyCode 还是 229
290
291
  if (event.nativeEvent.isComposing || event.keyCode === 229) return;
291
292
  const input = event.currentTarget;
292
293
 
@@ -82,7 +82,15 @@ const toneText: Record<Exclude<ToastTone, "neutral">, string> = {
82
82
 
83
83
  /*
84
84
  * 水平居中 + 跟着手指走的位移(可以往下或往右划走)。
85
- * 新旧两条在同一个位置交替淡入淡出,所以用绝对定位叠在一起
85
+ *
86
+ * 写在 transform 上,不能写在 translate 上:基元在拖动的时候自己往元素上写内联的
87
+ * transform(从元素现在的 transform 出发,加上指针走过的距离),并把过渡关掉;松手
88
+ * 之后交还给这里的变量。位置写在别的属性上的话两处会叠加——提示走的是手指的两倍。
89
+ *
90
+ * 进场的位移另写在 translate 上(只在起点有):两个属性各管各的,"滑入"的过渡碰不到
91
+ * "跟手"的那一份。底部的从下面来;正中的是官网的做法,只有透明度。
92
+ *
93
+ * 新旧两条在同一个位置交替,所以用绝对定位叠在一起:旧的原地淡出,新的升上来
86
94
  */
87
95
  const placementClass: Record<
88
96
  ToastPlacement,
@@ -90,11 +98,11 @@ const placementClass: Record<
90
98
  > = {
91
99
  bottom: {
92
100
  viewport: "bottom-6",
93
- root: "bottom-0 translate-y-[var(--toast-swipe-movement-y,0px)]",
101
+ root: "bottom-0 transform-[translate(calc(-50%+var(--toast-swipe-movement-x,0px)),var(--toast-swipe-movement-y,0px))] data-starting-style:translate-y-(--motion-shift-lg)",
94
102
  },
95
103
  center: {
96
104
  viewport: "top-1/2",
97
- root: "top-0 translate-y-[calc(-50%+var(--toast-swipe-movement-y,0px))]",
105
+ root: "top-0 transform-[translate(calc(-50%+var(--toast-swipe-movement-x,0px)),calc(-50%+var(--toast-swipe-movement-y,0px)))]",
98
106
  },
99
107
  };
100
108
 
@@ -169,11 +177,16 @@ function ToastHost({ placement, label, closeLabel, children }: ToastHostProps) {
169
177
  // 黑底白字不随主题变:整条做成暗色的局部主题,图形和焦点环按深色底取值
170
178
  data-theme="dark"
171
179
  className={cn(
172
- "pointer-events-auto absolute left-1/2 w-max max-w-full translate-x-[calc(-50%+var(--toast-swipe-movement-x,0px))]",
180
+ "pointer-events-auto absolute left-1/2 w-max max-w-full",
173
181
  // 边线是给暗色页面的:那里黑底和页面几乎一样深,要靠它把提示勾出来。
174
182
  // 亮色页面上它和提示的底几乎同色,看不出来
175
183
  "border border-line bg-black/80 text-base leading-[1.4] text-white",
176
- "transition-opacity duration-(--duration-fast) ease-standard data-ending-style:opacity-0 data-starting-style:opacity-0",
184
+ // 三样各走各的:淡入淡出 200ms;进场的位移 300ms;没划够就松手,滑回原位 200ms。
185
+ // 拖动中基元把过渡整个关掉,位置贴着手指
186
+ "transition-[opacity,translate,transform] duration-[var(--duration-fast),var(--duration-base),var(--duration-fast)] ease-[var(--ease-standard),var(--ease-exit),var(--ease-standard)]",
187
+ "data-ending-style:opacity-0 data-starting-style:opacity-0",
188
+ // 拖的时候不选中文字:用鼠标拖会把字选上,下一次再拖,浏览器当成"拖这段字",手势就被取消了
189
+ "data-swiping:select-none",
177
190
  // 焦点环画在里面:它是按深色底取的颜色,画到外面会压在亮色的页面上
178
191
  focusRingInset,
179
192
  placementClass[placement].root,
@@ -99,6 +99,7 @@ export function Toc({
99
99
  return active;
100
100
  },
101
101
  // count 没在里面用到:它变了这个函数就换一个,滚动的监听跟着重新接、重新算一次
102
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
102
103
  [target, offset, count],
103
104
  );
104
105
  const spied = useScrollPosition<string | null>(target, read, null);
@@ -11,6 +11,7 @@ import {
11
11
  import { createPortal } from "react-dom";
12
12
  import { usePortalScope } from "../../hooks/usePortalScope";
13
13
  import { cn } from "../../lib/cn";
14
+ import { enterFromSide } from "../../lib/motion";
14
15
 
15
16
  export type TooltipSide = "top" | "bottom" | "left" | "right";
16
17
  export type TooltipAlign = "start" | "center" | "end";
@@ -152,8 +153,11 @@ export function Tooltip({
152
153
  className={cn(
153
154
  // 透明的边线:高对比模式下背景色被系统覆盖,边线会自己显形
154
155
  "relative max-w-64 border border-transparent bg-surface-raised px-2.5 py-1.5 text-sm wrap-anywhere text-ink",
155
- "origin-(--transform-origin) transition-opacity duration-(--duration-fast) ease-standard",
156
+ // 进场从触发元素那一侧挪过来 4px,退场只淡出。小件:进出都是 200ms。
157
+ // data-instant(键盘聚焦、移到相邻的提示)时基元要它立刻出现,整个不过渡
158
+ "origin-(--transform-origin) transition-[opacity,translate] duration-(--duration-fast) ease-standard",
156
159
  "data-ending-style:opacity-0 data-instant:transition-none data-starting-style:opacity-0",
160
+ enterFromSide,
157
161
  className,
158
162
  )}
159
163
  >
@@ -16,6 +16,12 @@ export type ViewfinderProps = ComponentProps<"div"> & {
16
16
  size?: ViewfinderSize;
17
17
  /** 正中放一个小准星,标出焦点 */
18
18
  crosshair?: boolean;
19
+ /**
20
+ * 挂上的时候取景角落位:八段短线各自从角上伸出来,播一次。
21
+ * 默认不动——取景角平时是静态的装饰。要它晚一点再动(等外面那一层先淡入完),
22
+ * 在外层设 `--bracket-delay`
23
+ */
24
+ animate?: boolean;
19
25
  /**
20
26
  * 四个角上的读数:坐标、比例、版本、时间码。
21
27
  * 必须是真实的数据;没有就不放。
@@ -44,6 +50,7 @@ const readoutPosition: Record<keyof ViewfinderReadouts, string> = {
44
50
  export function Viewfinder({
45
51
  size = "sm",
46
52
  crosshair = false,
53
+ animate = false,
47
54
  readouts,
48
55
  className,
49
56
  children,
@@ -58,6 +65,8 @@ export function Viewfinder({
58
65
  decor,
59
66
  "corner-brackets absolute inset-3 [--bracket-color:currentColor] [--bracket-offset:0px] [--bracket-width:1.5px]",
60
67
  armClass[size],
68
+ animate &&
69
+ "after:animate-bracket-in after:[animation-delay:var(--bracket-delay,0s)]",
61
70
  )}
62
71
  />
63
72
  {crosshair && (
package/src/index.ts CHANGED
@@ -28,7 +28,9 @@ export * from "./components/flyout-bar";
28
28
  export * from "./components/ghost-text";
29
29
  export * from "./components/hatch";
30
30
  export * from "./components/hazard-stripe";
31
+ export * from "./components/hover-card";
31
32
  export * from "./components/icon-button";
33
+ export * from "./components/image-viewer";
32
34
  export * from "./components/input";
33
35
  export * from "./components/item-slot";
34
36
  export * from "./components/kbd";
@@ -39,6 +41,8 @@ export * from "./components/media-card";
39
41
  export * from "./components/nav-action";
40
42
  export * from "./components/nav-menu";
41
43
  export * from "./components/navigator";
44
+ export * from "./components/otp-input";
45
+ export * from "./components/page-header";
42
46
  export * from "./components/pagination";
43
47
  export * from "./components/panel";
44
48
  export * from "./components/play-button";
@@ -48,6 +52,7 @@ export * from "./components/radio";
48
52
  export * from "./components/rec-indicator";
49
53
  export * from "./components/registration-strip";
50
54
  export * from "./components/schedule";
55
+ export * from "./components/scroll-area";
51
56
  export * from "./components/scroll-hint";
52
57
  export * from "./components/section-title";
53
58
  export * from "./components/segmented-control";
package/src/lib/README.md CHANGED
@@ -9,6 +9,7 @@
9
9
  | `merge-refs.ts` | `mergeRefs()`:组件自己要用节点、同时还得把它交给使用方时,把几个 ref 合成一个 |
10
10
  | `defined.ts` | `defined()`:去掉值是 `undefined` 的属性。把控件的属性合并到使用方给的元素上(`render`)之前用,免得把那个元素自己写的盖掉 |
11
11
  | `decor.ts` | `decor`:装饰层共用的类 |
12
+ | `motion.ts` | `enterFromSide`:浮层面板进场的方向位移——按基元给的 `data-side`,起点往触发元素那一侧偏 `--motion-shift`。见 [动效](../../../../docs/design/foundations/motion.md#方向位移) |
12
13
  | `date.ts` | 日期的纯函数:加减日和月、比较、把两天排成先后、夹到范围内、月历的六行七列。日期一律是 `YYYY-MM-DD` 的字符串,全部按 UTC 算,不碰本地时区。`Calendar`、`DatePicker`、`Schedule` 共用,不对外导出 |
13
14
  | `file.ts` | 文件的纯函数:按 `accept` 认类型(扩展名、MIME、`image/*` 通配)、把字节数写成 `1.2 MB`、取扩展名、判断是不是同一个文件。`FileUpload` 用,不对外导出 |
14
15
 
package/src/lib/cn.ts CHANGED
@@ -24,6 +24,8 @@ const twMerge = extendTailwindMerge({
24
24
  "turn-in",
25
25
  "fade-in",
26
26
  "indeterminate",
27
+ "bracket-in",
28
+ "shift-in",
27
29
  ],
28
30
  },
29
31
  },
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 浮层从触发它的那一侧来:进场的起点往触发处偏 `--motion-shift`,走完归位。
3
+ * 用在基元的浮层面板上——它们带着 `data-side`(面板在触发元素的哪一侧)。
4
+ *
5
+ * 位移只写在 `data-starting-style` 上:退场不走位移,只淡出。
6
+ * 用的地方自己给 `translate` 加过渡。`inline-start` / `inline-end` 按从左往右的排版算。
7
+ */
8
+ export const enterFromSide = [
9
+ "data-starting-style:data-[side=bottom]:-translate-y-(--motion-shift)",
10
+ "data-starting-style:data-[side=top]:translate-y-(--motion-shift)",
11
+ "data-starting-style:data-[side=right]:-translate-x-(--motion-shift)",
12
+ "data-starting-style:data-[side=left]:translate-x-(--motion-shift)",
13
+ "data-starting-style:data-[side=inline-end]:-translate-x-(--motion-shift)",
14
+ "data-starting-style:data-[side=inline-start]:translate-x-(--motion-shift)",
15
+ ].join(" ");
@@ -336,6 +336,13 @@
336
336
  --duration-slow: 400ms;
337
337
  --duration-slower: 600ms;
338
338
 
339
+ /*
340
+ * 进场的方向位移:浮层从触发它的那一侧来。只是提示方向,不是滑行。
341
+ * 大的那一档给大面积的和贴着视口边缘出来的(全屏菜单的栏目、轻提示)
342
+ */
343
+ --motion-shift: 4px;
344
+ --motion-shift-lg: 8px;
345
+
339
346
  /* —— 母题尺寸 —— */
340
347
  --cut-sm: 6px;
341
348
  --cut-md: 10px;
@@ -368,6 +375,8 @@
368
375
  --animate-fade-in: ef-fade-in 300ms ease-out both;
369
376
  --animate-indeterminate: ef-indeterminate 1.2s linear infinite alternate;
370
377
  --animate-pulse: ef-pulse 2s ease-in-out infinite;
378
+ --animate-bracket-in: ef-bracket-in 200ms ease-out both;
379
+ --animate-shift-in: ef-shift-in 300ms ease-out both;
371
380
 
372
381
  /* 三次短促闪烁后点亮(官网实测的关键帧节奏) */
373
382
  @keyframes ef-flicker {
@@ -507,6 +516,37 @@
507
516
  opacity: 0.6;
508
517
  }
509
518
  }
519
+
520
+ /*
521
+ * 角括号落位:八段短线各自从角的顶点伸到满长。写成 after:animate-bracket-in,
522
+ * 和 corner-brackets 一起用——八层的顺序(横、竖交替)跟着 utilities.css 里那八层背景走。
523
+ * 只写起点:终点就是括号自己的尺寸。只动背景的尺寸,括号不会比平时多占一点地方
524
+ */
525
+ @keyframes ef-bracket-in {
526
+ from {
527
+ background-size:
528
+ 0 var(--_w),
529
+ var(--_w) 0,
530
+ 0 var(--_w),
531
+ var(--_w) 0,
532
+ 0 var(--_w),
533
+ var(--_w) 0,
534
+ 0 var(--_w),
535
+ var(--_w) 0;
536
+ }
537
+ }
538
+
539
+ /*
540
+ * 位移入场:从偏着的地方淡入归位。往哪边偏、偏多少由用的地方给
541
+ * (--shift-x / --shift-y,量用 --motion-shift 那两档),不给就只是淡入。
542
+ * 只写起点:终点就是元素自己的样子
543
+ */
544
+ @keyframes ef-shift-in {
545
+ from {
546
+ opacity: 0;
547
+ translate: var(--shift-x, 0) var(--shift-y, 0);
548
+ }
549
+ }
510
550
  }
511
551
 
512
552
  /* ------------------------------------------------------------------ */
@@ -563,6 +603,9 @@
563
603
  animation-duration: 0.01ms !important;
564
604
  animation-iteration-count: 1 !important;
565
605
  transition-duration: 0.01ms !important;
606
+ /* 延迟也归零:不然错开的入场会变成一项一项蹦出来 */
607
+ animation-delay: 0s !important;
608
+ transition-delay: 0s !important;
566
609
  scroll-behavior: auto !important;
567
610
  }
568
611
  }
@@ -121,7 +121,8 @@
121
121
  /* ------------------------------------------------------------------ */
122
122
  /* 角括号:四角各一个 L 形短线,用 ::after 的背景画,不占用 outline。 */
123
123
  /* 宿主要自己定位(relative 或 absolute);括号画在宿主之外, */
124
- /* 所以宿主和它的裁切祖先之间要留出 --bracket-offset 的空隙 */
124
+ /* 所以宿主和它的裁切祖先之间要留出 --bracket-offset 的空隙。 */
125
+ /* 要它出现时"落位",再加 after:animate-bracket-in(关键帧在 theme.css) */
125
126
  /* ------------------------------------------------------------------ */
126
127
 
127
128
  @utility corner-brackets {