@endfield-ui/react 0.3.0 → 1.0.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 (69) hide show
  1. package/NOTICE.md +2 -2
  2. package/README.md +103 -8
  3. package/dist/index.d.ts +69 -15
  4. package/dist/index.js +1102 -237
  5. package/dist/styles.css +1 -1
  6. package/package.json +1 -1
  7. package/src/components/README.md +11 -2
  8. package/src/components/alert/Alert.tsx +63 -1
  9. package/src/components/avatar/Avatar.tsx +5 -2
  10. package/src/components/bracket-title/BracketTitle.tsx +32 -3
  11. package/src/components/button/Button.tsx +37 -5
  12. package/src/components/calendar/Calendar.tsx +49 -5
  13. package/src/components/checkbox/Checkbox.tsx +1 -0
  14. package/src/components/checkbox/choice-style.ts +9 -4
  15. package/src/components/chip/capsule-style.ts +18 -3
  16. package/src/components/combobox/Combobox.tsx +10 -0
  17. package/src/components/completion-banner/CompletionBanner.tsx +30 -2
  18. package/src/components/dash-indicator/DashIndicator.tsx +5 -2
  19. package/src/components/data-row/DataRow.tsx +2 -1
  20. package/src/components/date-picker/DateField.tsx +5 -1
  21. package/src/components/dropdown-menu/MenuCheck.tsx +4 -2
  22. package/src/components/dropdown-menu/menu-style.ts +13 -3
  23. package/src/components/empty-state/EmptyState.tsx +5 -0
  24. package/src/components/field/Field.tsx +39 -7
  25. package/src/components/file-upload/FileUpload.tsx +78 -19
  26. package/src/components/icon-button/IconButton.tsx +18 -5
  27. package/src/components/input/control-box.ts +23 -2
  28. package/src/components/input/value-chip.ts +3 -2
  29. package/src/components/loader/Loader.tsx +12 -41
  30. package/src/components/nav-menu/NavMenu.tsx +3 -2
  31. package/src/components/otp-input/OtpInput.tsx +8 -2
  32. package/src/components/page-header/PageHeader.tsx +39 -3
  33. package/src/components/pagination/Pagination.tsx +2 -0
  34. package/src/components/progress/Progress.tsx +10 -3
  35. package/src/components/progress/ProgressRing.tsx +19 -4
  36. package/src/components/radio/Radio.tsx +2 -0
  37. package/src/components/rolling-number/RollingNumber.tsx +125 -0
  38. package/src/components/rolling-number/index.ts +1 -0
  39. package/src/components/schedule/Schedule.tsx +5 -3
  40. package/src/components/segmented-control/SegmentedControl.tsx +25 -4
  41. package/src/components/skeleton/Skeleton.tsx +4 -1
  42. package/src/components/slider/Slider.tsx +21 -4
  43. package/src/components/sparkline/Sparkline.tsx +29 -5
  44. package/src/components/spinner/Spinner.tsx +6 -2
  45. package/src/components/stepper/Stepper.tsx +4 -0
  46. package/src/components/steps/Steps.tsx +18 -6
  47. package/src/components/switch/Switch.tsx +26 -2
  48. package/src/components/table/Table.tsx +33 -9
  49. package/src/components/tabs/Tabs.tsx +55 -9
  50. package/src/components/tag-input/TagInput.tsx +82 -43
  51. package/src/components/textarea/Textarea.tsx +2 -1
  52. package/src/components/timeline/Timeline.tsx +4 -3
  53. package/src/components/toc/Toc.tsx +89 -7
  54. package/src/components/toolbar/Toolbar.tsx +24 -0
  55. package/src/hooks/README.md +8 -2
  56. package/src/hooks/index.ts +1 -0
  57. package/src/hooks/useAddedKeys.ts +26 -0
  58. package/src/hooks/useFlip.ts +211 -0
  59. package/src/hooks/useIndicator.ts +135 -0
  60. package/src/hooks/useLingering.ts +122 -0
  61. package/src/hooks/usePresence.ts +100 -0
  62. package/src/hooks/useReducedMotion.ts +26 -0
  63. package/src/hooks/useTheme.ts +44 -4
  64. package/src/index.ts +1 -0
  65. package/src/lib/README.md +2 -1
  66. package/src/lib/cn.ts +1 -0
  67. package/src/lib/inline-style.ts +12 -0
  68. package/src/lib/motion.ts +75 -0
  69. package/src/styles/theme.css +38 -2
package/dist/index.js CHANGED
@@ -78,7 +78,8 @@ const twMerge = extendTailwindMerge({ extend: { theme: {
78
78
  "fade-in",
79
79
  "indeterminate",
80
80
  "bracket-in",
81
- "shift-in"
81
+ "shift-in",
82
+ "wipe-in"
82
83
  ]
83
84
  } } });
84
85
  function cn(...inputs) {
@@ -170,6 +171,74 @@ function AccordionItem({ value, title, extra, disabled, className, children, ...
170
171
  });
171
172
  }
172
173
  //#endregion
174
+ //#region src/hooks/usePresence.ts
175
+ /** 循环的动画(加载指示那一类)等不到头,不等 */
176
+ function ends(animation) {
177
+ return animation.effect?.getComputedTiming().iterations !== Infinity;
178
+ }
179
+ /**
180
+ * 等这个元素上**正在跑的**、有尽头的过渡和动画走完,再调 `done`;一个都没有
181
+ * (没有写退场的样式、测试环境里没有动效)就当场调。返回的函数取消这一次等待。
182
+ */
183
+ function afterMotion(node, done) {
184
+ let live = true;
185
+ const finish = () => {
186
+ if (live) done();
187
+ };
188
+ const running = typeof node?.getAnimations === "function" ? node.getAnimations().filter(ends) : [];
189
+ if (running.length === 0) finish();
190
+ else Promise.allSettled(running.map((animation) => animation.finished)).then(finish);
191
+ return () => {
192
+ live = false;
193
+ };
194
+ }
195
+ /**
196
+ * 退场后再卸载:`open` 变成假之后元素先留着,等它身上的动效走完才卸载。
197
+ *
198
+ * 等的是这个元素上**正在跑的**过渡和动画,不另抄一份时长;一个都没有
199
+ * (没有写退场的样式、测试环境里没有动效)就当场卸载。退场途中又打开:不卸载,
200
+ * 过渡自己折回去。
201
+ */
202
+ function usePresence(open, { onLeave, onExited } = {}) {
203
+ const ref = useRef(null);
204
+ const [mounted, setMounted] = useState(open);
205
+ const [entered, setEntered] = useState(false);
206
+ if (open && !mounted) {
207
+ setMounted(true);
208
+ setEntered(true);
209
+ }
210
+ const leaving = !open && mounted;
211
+ const handlers = useRef({
212
+ onLeave,
213
+ onExited
214
+ });
215
+ useLayoutEffect(() => {
216
+ handlers.current = {
217
+ onLeave,
218
+ onExited
219
+ };
220
+ });
221
+ useLayoutEffect(() => {
222
+ if (!leaving) return;
223
+ const node = ref.current;
224
+ const undo = node ? handlers.current.onLeave?.(node) : void 0;
225
+ const cancel = afterMotion(node, () => {
226
+ setMounted(false);
227
+ handlers.current.onExited?.();
228
+ });
229
+ return () => {
230
+ cancel();
231
+ undo?.();
232
+ };
233
+ }, [leaving]);
234
+ return {
235
+ ref,
236
+ mounted,
237
+ leaving,
238
+ entered
239
+ };
240
+ }
241
+ //#endregion
173
242
  //#region src/icons/Close.tsx
174
243
  const Close = createIcon("Close", /* @__PURE__ */ jsx("path", { d: "M5 5l14 14M19 5L5 19" }));
175
244
  //#endregion
@@ -193,6 +262,15 @@ const StatusSuccess = createIcon("StatusSuccess", /* @__PURE__ */ jsxs(Fragment$
193
262
  /** 三角:警告 */
194
263
  const StatusWarning = createIcon("StatusWarning", /* @__PURE__ */ jsx("path", { d: "M12 4l9 16H3zM12 10v5M12 16.5v2" }));
195
264
  //#endregion
265
+ //#region src/lib/merge-refs.ts
266
+ /** 把几个 ref 合成一个:组件自己要用节点,同时还得把它交给使用方。 */
267
+ function mergeRefs(...refs) {
268
+ return (node) => {
269
+ for (const ref of refs) if (typeof ref === "function") ref(node);
270
+ else if (ref) ref.current = node;
271
+ };
272
+ }
273
+ //#endregion
196
274
  //#region src/components/alert/Alert.tsx
197
275
  const toneIcon$1 = {
198
276
  info: StatusInfo,
@@ -212,15 +290,43 @@ const toneText$1 = {
212
290
  warning: "text-warning",
213
291
  danger: "text-danger"
214
292
  };
293
+ function collapseOut(node) {
294
+ const { style } = node;
295
+ const before = style.cssText;
296
+ let gap = 0;
297
+ if (node.parentElement) {
298
+ const layout = getComputedStyle(node.parentElement);
299
+ if (layout.display.includes("grid") || layout.display.includes("flex") && layout.flexDirection.startsWith("column")) gap = Number.parseFloat(layout.rowGap) || 0;
300
+ }
301
+ style.height = `${node.getBoundingClientRect().height}px`;
302
+ node.getBoundingClientRect();
303
+ style.height = "0px";
304
+ style.minHeight = "0px";
305
+ style.paddingBlock = "0px";
306
+ style.borderBlockWidth = "0px";
307
+ style.marginTop = "0px";
308
+ style.marginBottom = `${-gap}px`;
309
+ style.opacity = "0";
310
+ return () => {
311
+ style.cssText = before;
312
+ };
313
+ }
215
314
  /** 页面内的一条常驻消息。需要用户处理的重要信息用它,不用会消失的轻提示。 */
216
- function Alert({ tone = "info", title, action, onClose, closeLabel = "关闭", icon, className, children, ...props }) {
315
+ function Alert({ tone = "info", title, action, onClose, open = true, onExited, closeLabel = "关闭", icon, className, ref, children, ...props }) {
316
+ const { ref: rootRef, mounted, leaving } = usePresence(open, {
317
+ onLeave: collapseOut,
318
+ onExited
319
+ });
320
+ if (!mounted) return null;
217
321
  const Icon = toneIcon$1[tone];
218
322
  const graphic = icon === void 0 ? /* @__PURE__ */ jsx(Icon, { size: 20 }) : icon;
219
323
  return /* @__PURE__ */ jsxs("div", {
220
324
  role: tone === "danger" || tone === "warning" ? "alert" : "status",
221
325
  ...props,
326
+ ref: mergeRefs(rootRef, ref),
222
327
  "data-tone": tone,
223
- className: cn("flex items-start gap-3 border border-l-4 border-line bg-surface-raised py-3 pr-3 pl-3 text-sm text-ink", toneBar[tone], className),
328
+ inert: leaving,
329
+ className: cn("flex items-start gap-3 border border-l-4 border-line bg-surface-raised py-3 pr-3 pl-3 text-sm text-ink", toneBar[tone], leaving && ["pointer-events-none overflow-hidden", "transition-[height,padding,margin,border-width,opacity] duration-(--duration-fast) ease-standard"], className),
224
330
  children: [
225
331
  graphic !== null && /* @__PURE__ */ jsx("span", {
226
332
  "aria-hidden": "true",
@@ -278,7 +384,7 @@ function Avatar({ name, src, alt = name ?? "", size = "md", selected = false, cl
278
384
  },
279
385
  "data-size": size,
280
386
  "data-selected": selected ? "" : void 0,
281
- className: cn("relative inline-flex shrink-0 items-center justify-center overflow-clip rounded-full bg-surface-muted font-medium text-ink-secondary select-none", sizeClass$13[size], selected && "shadow-[0_0_0_2px_var(--ef-surface),0_0_0_5px_var(--ef-action),0_0_0_6px_var(--ef-ink)]", className),
387
+ className: cn("relative inline-flex shrink-0 items-center justify-center overflow-clip rounded-full bg-surface-muted font-medium text-ink-secondary select-none", sizeClass$13[size], selected && "shadow-[0_0_0_2px_var(--ef-surface),0_0_0_5px_var(--ef-action),0_0_0_6px_var(--ef-ink)] forced-colors:border-[3px] forced-colors:border-[Highlight]", className),
282
388
  children: [src && /* @__PURE__ */ jsx(Avatar$1.Image, {
283
389
  src,
284
390
  alt: "",
@@ -319,7 +425,7 @@ const ChevronRight = createIcon("ChevronRight", /* @__PURE__ */ jsx("path", { d:
319
425
  //#endregion
320
426
  //#region src/components/icon-button/IconButton.tsx
321
427
  const sizeClass$12 = {
322
- sm: "size-8 after:absolute after:-inset-1 after:content-[''] [&_svg]:size-4",
428
+ sm: "size-8 after:absolute after:-inset-1 after:content-[''] forced-colors:after:-inset-[5px] [&_svg]:size-4",
323
429
  md: "size-10 [&_svg]:size-5",
324
430
  lg: "size-12 [&_svg]:size-6"
325
431
  };
@@ -335,6 +441,10 @@ const pressedClass = {
335
441
  accent: "bg-action text-on-action active:bg-action-pressed",
336
442
  inverse: "bg-surface-inverse text-accent-ink-inverse"
337
443
  };
444
+ const outlineForced = "forced-colors:border";
445
+ const enabledForced = "forced-colors:border-[ButtonText]";
446
+ const pressedForced = "forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]";
447
+ const disabledForced = "forced-colors:border-[GrayText]";
338
448
  const disabledClass = {
339
449
  plain: "cursor-not-allowed bg-surface-sunken text-ink-disabled",
340
450
  floating: "cursor-not-allowed rounded-full bg-neutral-50 text-neutral-400 shadow-sm",
@@ -349,7 +459,7 @@ function IconButton({ variant = "plain", size = "md", pressed, disabled = false,
349
459
  "aria-pressed": pressed,
350
460
  "data-variant": variant,
351
461
  "data-size": size,
352
- className: cn("relative inline-flex shrink-0 items-center justify-center", "transition-colors duration-(--duration-fast) ease-standard", focusRing, sizeClass$12[size], disabled ? disabledClass[variant] : pressed ? pressedClass[variant] : enabledClass$1[variant], className),
462
+ className: cn("relative inline-flex shrink-0 items-center justify-center", "transition-colors duration-(--duration-fast) ease-standard", focusRing, sizeClass$12[size], outlineForced, disabled ? [disabledClass[variant], disabledForced] : pressed ? [pressedClass[variant], pressedForced] : [enabledClass$1[variant], enabledForced], className),
353
463
  children
354
464
  });
355
465
  }
@@ -608,27 +718,69 @@ function Badge({ count, max = 99, showZero = false, dot = false, label, classNam
608
718
  });
609
719
  }
610
720
  //#endregion
721
+ //#region src/hooks/useInView.ts
722
+ /**
723
+ * 元素第一次进入视口后返回 true,之后不再变回 false——入场动画只播一次。
724
+ */
725
+ function useInView({ disabled = false, rootMargin = "0px", threshold = 0 } = {}) {
726
+ const ref = useRef(null);
727
+ const [inView, setInView] = useState(false);
728
+ useEffect(() => {
729
+ const node = ref.current;
730
+ if (disabled || inView || !node) return;
731
+ if (typeof IntersectionObserver === "undefined") {
732
+ setInView(true);
733
+ return;
734
+ }
735
+ const observer = new IntersectionObserver((entries) => {
736
+ if (entries.some((entry) => entry.isIntersecting)) {
737
+ setInView(true);
738
+ observer.disconnect();
739
+ }
740
+ }, {
741
+ rootMargin,
742
+ threshold
743
+ });
744
+ observer.observe(node);
745
+ return () => observer.disconnect();
746
+ }, [
747
+ disabled,
748
+ inView,
749
+ rootMargin,
750
+ threshold
751
+ ]);
752
+ return [ref, inView];
753
+ }
754
+ //#endregion
611
755
  //#region src/components/bracket-title/BracketTitle.tsx
612
756
  const bracket = "font-medium text-ink-tertiary";
613
757
  /**
614
758
  * 方括号标题 `[ 名称 ]`:人物、地点、物品的名称——"一个被归档的条目"。
615
759
  * 普通的分节标题用 `SectionTitle`。字号用 `className` 改,默认是名称用的 `text-4xl`。
616
760
  */
617
- function BracketTitle({ level = 3, className, children, ...props }) {
761
+ function BracketTitle({ level = 3, animate = true, className, children, ref, ...props }) {
618
762
  const Heading = `h${level}`;
763
+ const [inViewRef, inView] = useInView({ disabled: !animate });
764
+ const pending = animate && !inView;
765
+ const playing = animate && inView;
766
+ const bracketEnter = cn(pending && "opacity-0", playing && "animate-fade-in");
619
767
  return /* @__PURE__ */ jsxs(Heading, {
620
768
  ...props,
769
+ ref: mergeRefs(inViewRef, ref),
621
770
  className: cn("text-4xl font-bold wrap-anywhere text-ink", className),
622
771
  children: [
623
772
  /* @__PURE__ */ jsx("span", {
624
773
  "aria-hidden": "true",
625
- className: cn(bracket, "mr-[0.4em]"),
774
+ className: cn(bracket, "mr-[0.4em]", bracketEnter),
626
775
  children: "["
627
776
  }),
628
- children,
777
+ /* @__PURE__ */ jsx("span", {
778
+ className: cn(pending && "opacity-0", playing && "animate-fade-in [animation-delay:100ms]") || void 0,
779
+ children
780
+ }),
629
781
  /* @__PURE__ */ jsx("span", {
630
782
  "aria-hidden": "true",
631
- className: cn(bracket, "ml-[0.4em]"),
783
+ className: cn(bracket, "ml-[0.4em]", bracketEnter),
632
784
  children: "]"
633
785
  })
634
786
  ]
@@ -747,7 +899,7 @@ const sizeClass$10 = {
747
899
  sm: "size-3",
748
900
  md: "size-4"
749
901
  };
750
- const square = "animate-spin bg-current motion-reduce:rotate-45 motion-reduce:animate-none";
902
+ const square = ["animate-spin bg-current motion-reduce:rotate-45 motion-reduce:animate-none", "forced-colors:bg-[CanvasText]"].join(" ");
751
903
  /**
752
904
  * 行内加载指示:一个匀速旋转的小方块,颜色继承文字。
753
905
  * 圆环形的用 `<ProgressRing size={16} />`;整块内容加载用 `Skeleton`。
@@ -837,13 +989,17 @@ const enabledClass = {
837
989
  back: "hatch hatch-mid bg-control text-white shadow-xs hover:bg-neutral-600 active:bg-control-pressed",
838
990
  text: "text-ink active:text-ink-secondary"
839
991
  };
992
+ const forcedOutline = "forced-colors:border";
993
+ const forcedEnabled = "forced-colors:border-[ButtonText]";
994
+ const forcedDisabled = "forced-colors:border-[GrayText] forced-colors:text-[GrayText]";
840
995
  function Button(props) {
841
996
  const { variant = "control", size = "md", iconStart, iconEnd, loading = false, loadingText, block = false, disabled = false, className, children, ...rest } = props;
842
997
  const inert = disabled || loading;
843
998
  const isText = variant === "text";
844
999
  const marker = markerColor[variant];
845
1000
  const iconSize = inlineIconSize[size];
846
- const classes = cn("group relative inline-flex shrink-0 items-center justify-center font-medium leading-none whitespace-nowrap select-none", "transition-colors duration-(--duration-fast) ease-standard", focusRing, sizeClass$9[size], marker && markerPadding[size], variant === "action" && actionPadding[size], variant === "back" && backPadding[size], isText && "h-auto border-b border-current py-1", disabled ? isText ? "cursor-not-allowed text-ink-disabled" : "cursor-not-allowed bg-disabled text-on-disabled" : enabledClass[variant], loading && "pointer-events-none", block && "flex w-full", className);
1001
+ const forcedMark = disabled ? "forced-colors:bg-[GrayText]" : "forced-colors:bg-[ButtonText]";
1002
+ const classes = cn("group relative inline-flex shrink-0 items-center justify-center font-medium leading-none whitespace-nowrap select-none", "transition-colors duration-(--duration-fast) ease-standard", focusRing, sizeClass$9[size], marker && markerPadding[size], variant === "action" && actionPadding[size], variant === "back" && backPadding[size], isText && "h-auto border-b border-current py-1", disabled ? isText ? "cursor-not-allowed text-ink-disabled" : "cursor-not-allowed bg-disabled text-on-disabled" : enabledClass[variant], !isText && forcedOutline, disabled ? forcedDisabled : !isText && forcedEnabled, loading && !disabled && "forced-colors:text-[ButtonText]", loading && "pointer-events-none", block && "flex w-full", className);
847
1003
  let start = iconStart;
848
1004
  let end = iconEnd;
849
1005
  if (variant === "back" && start === void 0) start = loading ? /* @__PURE__ */ jsx("span", {
@@ -854,7 +1010,7 @@ function Button(props) {
854
1010
  },
855
1011
  children: /* @__PURE__ */ jsx(Spinner, {
856
1012
  label: null,
857
- className: "size-2"
1013
+ className: cn("size-2", forcedMark)
858
1014
  })
859
1015
  }) : /* @__PURE__ */ jsx(ChevronLeft, { size: iconSize });
860
1016
  if (isText && end === void 0) end = loading ? /* @__PURE__ */ jsx("span", {
@@ -865,7 +1021,7 @@ function Button(props) {
865
1021
  },
866
1022
  children: /* @__PURE__ */ jsx(Spinner, {
867
1023
  label: null,
868
- className: "size-2"
1024
+ className: cn("size-2", forcedMark)
869
1025
  })
870
1026
  }) : /* @__PURE__ */ jsx(ArrowCorner, {
871
1027
  size: iconSize,
@@ -888,11 +1044,11 @@ function Button(props) {
888
1044
  className: cn("pointer-events-none absolute top-1/2 -translate-y-1/2", markerSpinner[size]),
889
1045
  children: /* @__PURE__ */ jsx(Spinner, {
890
1046
  label: null,
891
- className: cn("block size-full", marker)
1047
+ className: cn("block size-full", marker, forcedMark)
892
1048
  })
893
1049
  }) : /* @__PURE__ */ jsx("span", {
894
1050
  "aria-hidden": "true",
895
- className: cn("marker-bar pointer-events-none absolute top-1/2 -translate-y-1/2", "transition-[clip-path,translate] duration-(--duration-base) ease-standard", markerBox[size], disabled ? "bg-on-disabled" : [
1051
+ className: cn("marker-bar pointer-events-none absolute top-1/2 -translate-y-1/2", "transition-[clip-path,translate] duration-(--duration-base) ease-standard", markerBox[size], forcedMark, disabled ? "bg-on-disabled" : [
896
1052
  marker,
897
1053
  "group-hover:marker-arrow group-active:marker-arrow",
898
1054
  markerShift[size]
@@ -903,12 +1059,12 @@ function Button(props) {
903
1059
  end,
904
1060
  variant === "action" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
905
1061
  "aria-hidden": "true",
906
- className: cn("pointer-events-none absolute top-[17.5%] h-[65%]", "transition-colors duration-(--duration-fast) ease-standard", actionDivider[size], disabled ? "bg-on-disabled/20" : "bg-on-action/10 group-hover:bg-on-action/15 group-active:bg-on-action/20")
1062
+ className: cn("pointer-events-none absolute top-[17.5%] h-[65%]", "transition-colors duration-(--duration-fast) ease-standard", actionDivider[size], disabled ? "bg-on-disabled/20" : ["bg-on-action/10 group-hover:bg-on-action/15 group-active:bg-on-action/20", "forced-colors:group-hover:bg-[ButtonText] forced-colors:group-active:bg-[ButtonText]"], forcedMark)
907
1063
  }), loading && /* @__PURE__ */ jsx("span", {
908
1064
  className: cn("pointer-events-none absolute top-1/2 -translate-y-1/2", actionSpinner[size]),
909
1065
  children: /* @__PURE__ */ jsx(Spinner, {
910
1066
  label: null,
911
- className: "block size-full"
1067
+ className: cn("block size-full", forcedMark)
912
1068
  })
913
1069
  })] })
914
1070
  ] });
@@ -1135,6 +1291,30 @@ function Calendar({ range = false, value: valueProp, defaultValue = null, onValu
1135
1291
  pendingFocus.current = false;
1136
1292
  gridRef.current?.querySelector(`[data-date="${focused}"]`)?.focus();
1137
1293
  }, [focused, month]);
1294
+ const turned = useRef({
1295
+ month,
1296
+ until: 0,
1297
+ length: 0
1298
+ });
1299
+ useLayoutEffect(() => {
1300
+ const last = turned.current;
1301
+ if (last.month === month) return;
1302
+ const forward = month > last.month;
1303
+ last.month = month;
1304
+ const grid = gridRef.current;
1305
+ if (!grid) return;
1306
+ const now = performance.now();
1307
+ delete grid.dataset.turn;
1308
+ if (now < last.until) {
1309
+ last.until = now + last.length;
1310
+ return;
1311
+ }
1312
+ grid.offsetWidth;
1313
+ grid.dataset.turn = forward ? "next" : "previous";
1314
+ const running = typeof grid.getAnimations === "function" ? grid.getAnimations()[0] : void 0;
1315
+ last.length = Number(running?.effect?.getComputedTiming().endTime ?? 0);
1316
+ last.until = now + last.length;
1317
+ }, [month]);
1138
1318
  const weeks = useMemo(() => monthGrid(month, weekStartsOn), [month, weekStartsOn]);
1139
1319
  const formats = useMemo(() => ({
1140
1320
  title: new Intl.DateTimeFormat(locale, {
@@ -1232,6 +1412,7 @@ function Calendar({ range = false, value: valueProp, defaultValue = null, onValu
1232
1412
  children: formats.weekdayShort.format(toDateObject(date))
1233
1413
  }, date)) }) }), /* @__PURE__ */ jsx("tbody", {
1234
1414
  ref: gridRef,
1415
+ className: "data-turn:animate-shift-in data-[turn=next]:[--shift-x:var(--motion-shift-lg)] data-[turn=previous]:[--shift-x:calc(var(--motion-shift-lg)*-1)]",
1235
1416
  onPointerLeave: () => setHovered(null),
1236
1417
  onKeyDown: (event) => {
1237
1418
  if (event.key === "Escape" && anchor !== null) {
@@ -1278,7 +1459,7 @@ function Calendar({ range = false, value: valueProp, defaultValue = null, onValu
1278
1459
  onClick: () => select(date),
1279
1460
  onFocus: () => setFocused(date),
1280
1461
  onPointerEnter: range ? () => setHovered(date) : void 0,
1281
- className: cn("relative flex size-10 items-center justify-center font-tech text-sm tabular-nums", "transition-colors duration-(--duration-fast) ease-standard", focusRingInset, endpoint ? "bg-surface-inverse font-bold text-ink-inverse" : [between ? "bg-ink/10" : previewing && "bg-ink/5", disabled ? "cursor-not-allowed text-ink-disabled line-through" : [outside ? "text-ink-tertiary" : "text-ink", between ? "hover:bg-ink/15" : "hover:bg-ink/5"]], isToday && "after:absolute after:bottom-1.5 after:left-1/2 after:h-0.5 after:w-3 after:-translate-x-1/2 after:content-['']", isToday && (endpoint ? "after:bg-accent-ink-inverse" : "after:bg-accent-ink")),
1462
+ className: cn("relative flex size-10 items-center justify-center font-tech text-sm tabular-nums", "transition-colors duration-(--duration-fast) ease-standard", focusRingInset, endpoint ? "bg-surface-inverse font-bold text-ink-inverse focus-visible:outline-accent-ink-inverse forced-colors:forced-color-adjust-none forced-colors:bg-[Highlight] forced-colors:text-[HighlightText] forced-colors:focus-visible:-outline-offset-4 forced-colors:focus-visible:outline-[HighlightText]" : [between ? "bg-ink/10 forced-colors:border-y-2 forced-colors:border-[Highlight]" : previewing && "bg-ink/5", disabled ? "cursor-not-allowed text-ink-disabled line-through" : [outside ? "text-ink-tertiary" : "text-ink", between ? "hover:bg-ink/15" : "hover:bg-ink/5"]], isToday && "after:absolute after:bottom-1.5 after:left-1/2 after:h-0.5 after:w-3 after:-translate-x-1/2 after:content-['']", isToday && (endpoint ? "after:bg-accent-ink-inverse forced-colors:after:bg-[HighlightText]" : "after:bg-accent-ink forced-colors:after:bg-[CanvasText]")),
1282
1463
  children: dateParts(date).day
1283
1464
  })
1284
1465
  }, date);
@@ -1309,7 +1490,7 @@ function DashIndicator({ count, index, className, ...props }) {
1309
1490
  className: cn("inline-flex items-center gap-1", className),
1310
1491
  children: Array.from({ length: total }, (_, position) => /* @__PURE__ */ jsx("span", {
1311
1492
  "data-current": position === current ? "" : void 0,
1312
- className: cn("h-0.5 transition-[width,background-color] duration-(--duration-base) ease-standard", position === current ? "w-8 bg-accent-ink" : "w-4 bg-line-strong")
1493
+ className: cn("h-0.5 transition-[width,background-color] duration-(--duration-base) ease-standard", position === current ? "w-8 bg-accent-ink forced-colors:bg-[Highlight]" : "w-4 bg-line-strong forced-colors:bg-[CanvasText]")
1313
1494
  }, position))
1314
1495
  });
1315
1496
  }
@@ -1510,27 +1691,18 @@ function CarouselSlide({ title: _title, description: _description, className, ch
1510
1691
  /** 对勾:已选、已完成 */
1511
1692
  const Check = createIcon("Check", /* @__PURE__ */ jsx("path", { d: "M4 12l6 6L20 6" }));
1512
1693
  //#endregion
1513
- //#region src/lib/merge-refs.ts
1514
- /** 把几个 ref 合成一个:组件自己要用节点,同时还得把它交给使用方。 */
1515
- function mergeRefs(...refs) {
1516
- return (node) => {
1517
- for (const ref of refs) if (typeof ref === "function") ref(node);
1518
- else if (ref) ref.current = node;
1519
- };
1520
- }
1521
- //#endregion
1522
1694
  //#region src/components/checkbox/choice-style.ts
1523
- const choiceLabel = "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";
1695
+ const choiceLabel = "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 forced-colors:has-disabled:text-[GrayText]";
1524
1696
  /** 控件的定位盒:高度等于一行文字,标签换行时控件仍然对齐第一行 */
1525
1697
  const choiceControl = "flex h-[1lh] shrink-0 items-center";
1526
- const choiceBox = "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";
1698
+ const choiceBox = ["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", "forced-colors:checked:border-[Highlight] forced-colors:checked:bg-[Highlight] forced-colors:disabled:border-[GrayText] forced-colors:disabled:bg-[Canvas] forced-colors:choice-diamond:checked:disabled:bg-[GrayText]"].join(" ");
1527
1699
  /** 方格 / 圆的定位盒:菱形比它小一圈,所以居中 */
1528
1700
  const choiceFrame = "relative inline-flex size-5 items-center justify-center";
1529
1701
  /**
1530
1702
  * 压在方格 / 圆正中的记号,默认透明,由 peer 的状态点亮。
1531
1703
  * 淡入淡出,和方格换底色的那一下同时走——底色有过渡而记号硬切,看起来是两件事
1532
1704
  */
1533
- const choiceMark = "pointer-events-none absolute inset-0 m-auto text-accent-ink-inverse opacity-0 transition-opacity duration-(--duration-fast) ease-standard peer-disabled:text-ink-disabled";
1705
+ const choiceMark = "pointer-events-none absolute inset-0 m-auto text-accent-ink-inverse opacity-0 transition-opacity duration-(--duration-fast) ease-standard peer-disabled:text-ink-disabled forced-colors:text-[HighlightText] forced-colors:peer-disabled:text-[GrayText]";
1534
1706
  //#endregion
1535
1707
  //#region src/components/checkbox/Checkbox.tsx
1536
1708
  /**
@@ -1559,7 +1731,7 @@ function Checkbox({ indeterminate = false, invalid = false, onCheckedChange, onC
1559
1731
  type: "checkbox",
1560
1732
  "aria-invalid": invalid || void 0,
1561
1733
  onChange: handleChange,
1562
- className: cn(choiceBox, "indeterminate:border-surface-inverse indeterminate:bg-surface-inverse", focusRing)
1734
+ className: cn(choiceBox, "indeterminate:border-surface-inverse indeterminate:bg-surface-inverse", "forced-colors:indeterminate:border-[Highlight] forced-colors:indeterminate:bg-[Highlight]", focusRing)
1563
1735
  }),
1564
1736
  /* @__PURE__ */ jsx(Check, {
1565
1737
  size: 14,
@@ -1586,11 +1758,20 @@ const capsuleSize = {
1586
1758
  sm: "h-7 px-3 text-xs",
1587
1759
  md: "h-8 px-4 text-sm"
1588
1760
  };
1761
+ const forcedSelected = "forced-colors:forced-color-adjust-none forced-colors:focus-visible:outline-[Highlight]";
1589
1762
  const capsuleState = {
1590
- selected: "border-transparent bg-surface-inverse text-ink-inverse",
1763
+ selected: [
1764
+ "border-transparent bg-surface-inverse text-ink-inverse",
1765
+ "forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
1766
+ forcedSelected
1767
+ ].join(" "),
1591
1768
  rest: "border-line-strong text-ink hover:bg-ink/5",
1592
1769
  disabled: "cursor-not-allowed border-line text-ink-disabled",
1593
- selectedDisabled: "cursor-not-allowed border-transparent bg-disabled text-on-disabled"
1770
+ selectedDisabled: [
1771
+ "cursor-not-allowed border-transparent bg-disabled text-on-disabled",
1772
+ "forced-colors:border-[GrayText] forced-colors:bg-[GrayText] forced-colors:text-[Canvas]",
1773
+ forcedSelected
1774
+ ].join(" ")
1594
1775
  };
1595
1776
  //#endregion
1596
1777
  //#region src/components/chip/FilterChip.tsx
@@ -1712,7 +1893,7 @@ function MenuCheck({ checked, disabled = false }) {
1712
1893
  "data-checked": checked ? "" : void 0,
1713
1894
  className: "flex size-4 shrink-0 items-center justify-center",
1714
1895
  children: /* @__PURE__ */ jsx("span", {
1715
- className: cn("flex size-4 items-center justify-center border-2 transition-colors duration-(--duration-fast) ease-standard choice-diamond:size-3 choice-diamond:rotate-45", disabled ? ["border-ink-disabled bg-surface-sunken text-ink-disabled", checked && "choice-diamond:bg-ink-disabled"] : checked ? "border-surface-inverse bg-surface-inverse text-accent-ink-inverse" : "border-line-strong bg-surface-sunken"),
1896
+ className: cn("flex size-4 items-center justify-center border-2 transition-colors duration-(--duration-fast) ease-standard choice-diamond:size-3 choice-diamond:rotate-45", disabled ? ["border-ink-disabled bg-surface-sunken text-ink-disabled", checked && "choice-diamond:bg-ink-disabled forced-colors:choice-diamond:bg-[GrayText]"] : checked ? "border-surface-inverse bg-surface-inverse text-accent-ink-inverse forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]" : "border-line-strong bg-surface-sunken"),
1716
1897
  children: checked && /* @__PURE__ */ jsx(Check, {
1717
1898
  size: 10,
1718
1899
  strokeWidth: 4,
@@ -1738,6 +1919,54 @@ const enterFromSide = [
1738
1919
  "data-starting-style:data-[side=inline-end]:-translate-x-(--motion-shift)",
1739
1920
  "data-starting-style:data-[side=inline-start]:translate-x-(--motion-shift)"
1740
1921
  ].join(" ");
1922
+ /**
1923
+ * 按高度收放,不量高度:外面这一层是只有一行的网格,行高在 `0fr` 和 `1fr` 之间过渡;
1924
+ * 里面再套一层 `collapseInner`,裁掉还放不下的那一截。表格的明细行、字段下面的错误文字
1925
+ * 都这么收放——内容后来变高了也跟着走。
1926
+ *
1927
+ * 展开 300ms、收起 200ms。收起由 `data-leaving` 触发,配 `usePresence`:收完再卸载。
1928
+ */
1929
+ const collapse = [
1930
+ "grid grid-rows-[1fr]",
1931
+ "transition-[grid-template-rows] duration-(--duration-base) ease-standard",
1932
+ "data-leaving:grid-rows-[0fr] data-leaving:duration-(--duration-fast)"
1933
+ ].join(" ");
1934
+ /** 只裁纵向:横向照旧,里面的东西该多宽还是多宽 */
1935
+ const collapseInner = "min-h-0 min-w-0 overflow-y-clip";
1936
+ /**
1937
+ * 选中指示:画在容器的 `::after` 上的一块东西,压在容器的底色之上、各项之下。
1938
+ * 位置和尺寸来自 `useIndicator` 写在容器上的变量;换了一项时滑过去(200ms,
1939
+ * 和各项换字色同一拍),没有当前项时原地淡出。
1940
+ *
1941
+ * 容器上没有 `data-indicator`(量不到)时不画——各项照旧自己画选中态,
1942
+ * 所以给各项的选中底加一个 `in-data-indicator:` 的变体把它让出来。
1943
+ * 这里只管定位和过渡:颜色、圆角由用的地方给,几何用 `indicatorBox` 或者自己写。
1944
+ */
1945
+ const indicator = [
1946
+ "relative isolate",
1947
+ "after:pointer-events-none after:absolute after:top-0 after:left-0 after:-z-1",
1948
+ "after:hidden data-indicator:after:block",
1949
+ "after:opacity-0 data-[indicator=on]:after:opacity-100",
1950
+ "after:transition-[translate,width,height,opacity] after:duration-(--duration-fast) after:ease-standard",
1951
+ "data-[indicator=off]:after:transition-[opacity]",
1952
+ "data-indicator-instant:after:transition-none"
1953
+ ].join(" ");
1954
+ /** 指示盖住当前项的整个盒子:分段选择的墨块、胶囊页签的底 */
1955
+ const indicatorBox = ["after:h-(--indicator-h) after:w-(--indicator-w)", "after:translate-x-(--indicator-x) after:translate-y-(--indicator-y)"].join(" ");
1956
+ /**
1957
+ * 补位(`useFlip`):一排里有一项走了,其余的从旧位置滑到新位置。
1958
+ *
1959
+ * `flipBox` 给那一排——走的那一项以它为参照钉在原处,并且压在别的项下面。
1960
+ * 少了一行它就矮了:高度由钩子量着收(200ms),这期间还在滑的项不露到它外面去;
1961
+ * 各行靠上排,撑着的那一段空在底下。
1962
+ * `flipLeaving` 给会走的每一项:带上 `data-leaving` 之后脱离排版(位置由钩子给)、
1963
+ * 原地淡出、点不到。
1964
+ *
1965
+ * 过渡由用的地方自己写:各项的 `translate`(滑回原位)、走的那一项的 `opacity`,
1966
+ * 都是 200ms——这几项多半本来就带着换色的过渡,得写进同一份清单里。
1967
+ */
1968
+ const flipBox = ["relative isolate content-start overflow-y-clip", "transition-[min-height] duration-(--duration-fast) ease-standard"].join(" ");
1969
+ const flipLeaving = ["data-leaving:pointer-events-none data-leaving:absolute data-leaving:-z-1", "data-leaving:opacity-0"].join(" ");
1741
1970
  //#endregion
1742
1971
  //#region src/components/dropdown-menu/menu-style.ts
1743
1972
  /** 选项要知道自己在哪种面板里,才知道"当前项"怎么画 */
@@ -1768,10 +1997,19 @@ const menuItem = [
1768
1997
  ].join(" ");
1769
1998
  /** 不是当前项的选项才有悬停底:当前项的底不能被悬停盖掉 */
1770
1999
  const menuItemHover = "data-highlighted:bg-ink/5";
1771
- /** 当前项(下拉选择的已选项、单选菜单项的选中项) */
2000
+ /**
2001
+ * 当前项(下拉选择的已选项、单选菜单项的选中项)。
2002
+ *
2003
+ * `strong` 的当前项是一整行黄底,而面板是深色的局部主题,焦点色也是黄的:
2004
+ * 画在里面的焦点环换成压得住黄底的颜色,并且再往里收 2px——那个颜色是墨色,
2005
+ * 贴着边的话和深色的面板分不开,看上去只是黄底小了一圈。
2006
+ *
2007
+ * 高对比模式下底色都被系统换掉:两种面板的当前项都是左缘一条系统"选中"色的竖条
2008
+ * (`strong` 平时没有这一条,只在这时候画)
2009
+ */
1772
2010
  const menuItemCurrent = {
1773
- plain: "bg-surface-muted font-medium before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-ink before:content-['']",
1774
- strong: "bg-action font-medium text-on-action"
2011
+ plain: "bg-surface-muted font-medium before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-ink before:content-[''] forced-colors:before:bg-[Highlight]",
2012
+ strong: "bg-action font-medium text-on-action focus-visible:-outline-offset-4 focus-visible:outline-on-action forced-colors:before:absolute forced-colors:before:inset-y-0 forced-colors:before:left-0 forced-colors:before:w-[3px] forced-colors:before:bg-[Highlight] forced-colors:before:content-['']"
1775
2013
  };
1776
2014
  const menuGroupLabel = "px-3 pt-2 pb-1 text-xs text-ink-tertiary";
1777
2015
  const menuSeparator = "my-1 h-px bg-line";
@@ -1807,6 +2045,9 @@ function Field({ label, help, error, required = false, disabled = false, group =
1807
2045
  const hasError = error !== void 0 && error !== null && error !== false;
1808
2046
  const hasHelp = help !== void 0 && help !== null && help !== false;
1809
2047
  const describedBy = [hasError && errorId, hasHelp && helpId].filter(Boolean).join(" ") || void 0;
2048
+ const { ref: errorRef, mounted: errorMounted, leaving: errorLeaving, entered: errorEntered } = usePresence(hasError);
2049
+ const [lastError, setLastError] = useState(error);
2050
+ if (hasError && !Object.is(lastError, error)) setLastError(error);
1810
2051
  const context = useMemo(() => ({
1811
2052
  controlId: group ? void 0 : id,
1812
2053
  labelId,
@@ -1830,16 +2071,25 @@ function Field({ label, help, error, required = false, disabled = false, group =
1830
2071
  "data-required": "",
1831
2072
  className: cn("ml-1.5 inline-block size-1.5 rotate-45 align-middle", disabled ? "bg-ink-disabled" : "bg-accent-ink")
1832
2073
  })] });
1833
- const messages = /* @__PURE__ */ jsxs(Fragment$1, { children: [hasError && /* @__PURE__ */ jsxs("p", {
1834
- id: errorId,
1835
- className: "mt-1.5 flex items-start gap-1.5 text-sm text-danger",
1836
- children: [/* @__PURE__ */ jsx(StatusDanger, {
1837
- size: 16,
1838
- className: "mt-0.5 shrink-0"
1839
- }), /* @__PURE__ */ jsx("span", {
1840
- className: "min-w-0 wrap-anywhere",
1841
- children: error
1842
- })]
2074
+ const messages = /* @__PURE__ */ jsxs(Fragment$1, { children: [errorMounted && /* @__PURE__ */ jsx("div", {
2075
+ ref: errorRef,
2076
+ "data-leaving": errorLeaving ? "" : void 0,
2077
+ "aria-hidden": errorLeaving || void 0,
2078
+ className: cn(collapse, "transition-[grid-template-rows,opacity] duration-(--duration-fast) data-leaving:opacity-0", errorEntered && ["starting:grid-rows-[0fr]", "starting:opacity-0"]),
2079
+ children: /* @__PURE__ */ jsx("div", {
2080
+ className: "min-h-0 min-w-0 overflow-y-clip",
2081
+ children: /* @__PURE__ */ jsxs("p", {
2082
+ id: errorId,
2083
+ className: "flex items-start gap-1.5 pt-1.5 text-sm text-danger",
2084
+ children: [/* @__PURE__ */ jsx(StatusDanger, {
2085
+ size: 16,
2086
+ className: "mt-0.5 shrink-0"
2087
+ }), /* @__PURE__ */ jsx("span", {
2088
+ className: "min-w-0 wrap-anywhere",
2089
+ children: hasError ? error : lastError
2090
+ })]
2091
+ })
2092
+ })
1843
2093
  }), hasHelp && /* @__PURE__ */ jsx("p", {
1844
2094
  id: helpId,
1845
2095
  className: cn("mt-1.5 text-sm", disabled ? "text-ink-disabled" : "text-ink-secondary"),
@@ -1908,10 +2158,16 @@ const fill$1 = {
1908
2158
  outline: "bg-surface"
1909
2159
  };
1910
2160
  function controlBox({ variant = "sunken", invalid, disabled, readOnly }) {
1911
- return cn("flex w-full min-w-0 text-ink", frame[variant], "transition-colors duration-(--duration-fast) ease-standard", focusRingWithin, readOnly ? "border-transparent bg-transparent" : fill$1[variant], disabled ? "cursor-not-allowed border-line text-ink-disabled" : invalid ? "border-danger" : !readOnly && "border-line-strong hover:not-focus-within:border-ink-secondary focus-within:border-ink");
2161
+ return cn("flex w-full min-w-0 text-ink", frame[variant], "transition-colors duration-(--duration-fast) ease-standard", focusRingWithin, readOnly ? "border-transparent bg-transparent" : fill$1[variant], readOnly && !disabled && !invalid && "forced-colors:border-[Canvas]", disabled ? "cursor-not-allowed border-line text-ink-disabled" : invalid ? "border-danger" : !readOnly && "border-line-strong hover:not-focus-within:border-ink-secondary focus-within:border-ink");
1912
2162
  }
1913
- /** 框里真正的 `<input>` / `<textarea>`:没有自己的底和边,焦点环画在外框上 */
1914
- const controlElement = "min-w-0 flex-1 bg-transparent outline-none placeholder:text-ink-tertiary disabled:cursor-not-allowed disabled:placeholder:text-ink-disabled";
2163
+ /**
2164
+ * 框里真正的 `<input>` / `<textarea>`:没有自己的底和边,焦点环画在外框上。
2165
+ *
2166
+ * 高对比模式下禁用的字色要自己写明:平时的 `ink-disabled` 写在外框上,系统换掉之后
2167
+ * 只有它自己认得的禁用控件才是灰字色——`<textarea>`、写了 `type` 的 `<input>` 认得,
2168
+ * 没写 `type` 的 `<input>`(输入框、组合框都是)不认,字和没禁用的一个颜色
2169
+ */
2170
+ const controlElement = "min-w-0 flex-1 bg-transparent outline-none placeholder:text-ink-tertiary disabled:cursor-not-allowed disabled:placeholder:text-ink-disabled forced-colors:disabled:text-[GrayText]";
1915
2171
  //#endregion
1916
2172
  //#region src/components/input/value-chip.ts
1917
2173
  /** 小块高 20 / 24 / 32px,跟着外框的三档 */
@@ -1920,9 +2176,9 @@ const valueChipSize = {
1920
2176
  md: "h-6 text-sm",
1921
2177
  lg: "h-8 text-base"
1922
2178
  };
1923
- const valueChip = "flex max-w-full min-w-0 cursor-default items-center bg-surface-muted pl-2 text-ink";
2179
+ const valueChip = "flex max-w-full min-w-0 cursor-default items-center bg-surface-muted pl-2 text-ink forced-colors:border forced-colors:border-[ButtonText]";
1924
2180
  /** 小块右端的删除叉:一个和小块等高的方钮 */
1925
- const valueChipRemove = "flex aspect-square h-full shrink-0 items-center justify-center text-ink-secondary hover:bg-line hover:text-ink";
2181
+ const valueChipRemove = "flex aspect-square h-full shrink-0 items-center justify-center text-ink-secondary hover:bg-line hover:text-ink forced-colors:in-data-disabled:text-[GrayText]";
1926
2182
  /** 和小块排在一起的那个输入框:和小块一样高,这一行才不会比小块高出一截 */
1927
2183
  const valueInputHeight = {
1928
2184
  sm: "h-5",
@@ -1948,6 +2204,7 @@ const currentHighlighted = {
1948
2204
  plain: "data-highlighted:bg-line",
1949
2205
  strong: "data-highlighted:bg-action-pressed"
1950
2206
  };
2207
+ const highlightedForced = "forced-colors:data-highlighted:outline-2 forced-colors:data-highlighted:-outline-offset-2 forced-colors:data-highlighted:outline-[Highlight]";
1951
2208
  const iconButton = "flex shrink-0 items-center justify-center self-stretch text-ink-secondary hover:text-ink disabled:cursor-not-allowed disabled:text-ink-disabled disabled:hover:text-ink-disabled";
1952
2209
  const byValue = (a, b) => a?.value === b?.value;
1953
2210
  /**
@@ -1979,7 +2236,7 @@ function Combobox(props) {
1979
2236
  const renderOption = (option) => /* @__PURE__ */ jsx(Combobox$1.Item, {
1980
2237
  value: option,
1981
2238
  disabled: option.disabled,
1982
- className: (state) => cn(menuItem, multiple ? [menuItemHover, state.selected && "font-medium"] : state.selected ? [menuItemCurrent[panelVariant], currentHighlighted[panelVariant]] : menuItemHover),
2239
+ className: (state) => cn(menuItem, multiple ? [menuItemHover, state.selected && "font-medium"] : state.selected ? [menuItemCurrent[panelVariant], currentHighlighted[panelVariant]] : menuItemHover, highlightedForced),
1983
2240
  render: multiple ? ({ children, ...itemProps }, state) => /* @__PURE__ */ jsxs("div", {
1984
2241
  ...itemProps,
1985
2242
  children: [/* @__PURE__ */ jsx(MenuCheck, {
@@ -2020,7 +2277,7 @@ function Combobox(props) {
2020
2277
  className: "flex min-w-0 flex-1 flex-wrap items-center gap-1 py-1 pl-2",
2021
2278
  children: /* @__PURE__ */ jsx(Combobox$1.Value, { children: (selected) => /* @__PURE__ */ jsxs(Fragment$1, { children: [selected.map((option) => /* @__PURE__ */ jsxs(Combobox$1.Chip, {
2022
2279
  "aria-label": option.label,
2023
- className: cn(valueChip, "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus", "data-disabled:text-ink-disabled", valueChipSize[size]),
2280
+ className: cn(valueChip, "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus", "data-disabled:text-ink-disabled", "forced-colors:data-disabled:border-[GrayText] forced-colors:data-disabled:text-[GrayText]", valueChipSize[size]),
2024
2281
  children: [/* @__PURE__ */ jsx("span", {
2025
2282
  className: "min-w-0 truncate",
2026
2283
  children: option.label
@@ -2158,13 +2415,17 @@ function GhostText({ variant = "hatch", className, ...props }) {
2158
2415
  * 底是反转块:亮色主题下是深底黄字,暗色主题下是浅底深字。
2159
2416
  * 整块是一个 `data-theme="inverse"` 的局部主题,`action` 里的按钮按这块底色取值。
2160
2417
  */
2161
- function CompletionBanner({ title, word = "COMPLETED", description, action, level = 3, className, children, ...props }) {
2418
+ function CompletionBanner({ title, word = "COMPLETED", description, action, level = 3, animate = true, className, children, ref, ...props }) {
2162
2419
  const Heading = `h${level}`;
2420
+ const [inViewRef, inView] = useInView({ disabled: !animate });
2421
+ const pending = animate && !inView;
2422
+ const playing = animate && inView;
2163
2423
  return /* @__PURE__ */ jsxs("div", {
2164
2424
  role: "status",
2165
2425
  "data-theme": "inverse",
2166
2426
  ...props,
2167
- className: cn("@container relative isolate overflow-clip bg-surface text-ink", className),
2427
+ ref: mergeRefs(inViewRef, ref),
2428
+ className: cn("@container relative isolate overflow-clip bg-surface text-ink", pending && "[clip-path:inset(0_100%_0_0)]", playing && "animate-wipe-in", className),
2168
2429
  children: [word !== null && /* @__PURE__ */ jsx(GhostText, {
2169
2430
  variant: "outline",
2170
2431
  className: "absolute -right-2 -bottom-3 -z-10 hidden text-5xl [--ghost-ink:color-mix(in_srgb,var(--ef-ink)_28%,transparent)] [mask-image:linear-gradient(to_left,black_45%,transparent)] @md:block",
@@ -2172,7 +2433,7 @@ function CompletionBanner({ title, word = "COMPLETED", description, action, leve
2172
2433
  }), /* @__PURE__ */ jsxs("div", {
2173
2434
  className: "flex flex-wrap items-center justify-between gap-x-6 gap-y-3 px-4 py-4 @md:px-6 @md:py-5",
2174
2435
  children: [/* @__PURE__ */ jsxs("div", {
2175
- className: "min-w-0 border-l-4 border-accent-ink pl-3",
2436
+ className: cn("min-w-0 border-l-4 border-accent-ink pl-3", playing && "animate-shift-in [animation-delay:200ms] [--shift-x:calc(var(--motion-shift-lg)*-1)]"),
2176
2437
  children: [
2177
2438
  /* @__PURE__ */ jsx(Heading, {
2178
2439
  className: "text-lg font-bold wrap-anywhere text-accent-ink @md:text-xl",
@@ -2185,7 +2446,7 @@ function CompletionBanner({ title, word = "COMPLETED", description, action, leve
2185
2446
  children
2186
2447
  ]
2187
2448
  }), action && /* @__PURE__ */ jsx("div", {
2188
- className: "shrink-0",
2449
+ className: cn("shrink-0", playing && "animate-fade-in [animation-delay:200ms]"),
2189
2450
  children: action
2190
2451
  })]
2191
2452
  })]
@@ -2365,6 +2626,8 @@ function sparklinePath(data, range = {}) {
2365
2626
  area: `${line}L${SIZE} ${SIZE}L0 ${SIZE}Z`
2366
2627
  };
2367
2628
  }
2629
+ const areaForced = "forced-colors:fill-[CanvasText]";
2630
+ const lineForced = "forced-colors:stroke-[CanvasText]";
2368
2631
  const areaClass = {
2369
2632
  info: "fill-data/75",
2370
2633
  danger: "fill-alert/75",
@@ -2381,11 +2644,14 @@ const lineClass = {
2381
2644
  * 行内的小型图表:没有坐标轴、网格和图例,只有一块面积或一条折线。
2382
2645
  * 撑满给它的盒子(默认高 32px、通宽)。它旁边应该有数字——图只是帮着看趋势。
2383
2646
  */
2384
- function Sparkline({ data, variant = "area", tone = "info", min, max, label, className, ...props }) {
2647
+ function Sparkline({ data, variant = "area", tone = "info", min, max, label, animate = true, className, ref, ...props }) {
2385
2648
  const path = sparklinePath(data, {
2386
2649
  min,
2387
2650
  max
2388
2651
  });
2652
+ const [inViewRef, inView] = useInView({ disabled: !animate });
2653
+ const pending = animate && !inView;
2654
+ const playing = animate && inView;
2389
2655
  return /* @__PURE__ */ jsx("svg", {
2390
2656
  viewBox: `0 0 ${SIZE} ${SIZE}`,
2391
2657
  preserveAspectRatio: "none",
@@ -2396,17 +2662,18 @@ function Sparkline({ data, variant = "area", tone = "info", min, max, label, cla
2396
2662
  "data-variant": variant,
2397
2663
  "data-tone": tone,
2398
2664
  ...props,
2399
- className: cn("block h-8 w-full overflow-visible", className),
2665
+ ref: mergeRefs(inViewRef, ref),
2666
+ className: cn("block h-8 w-full overflow-visible", pending && "[clip-path:inset(0_100%_0_0)]", playing && "animate-wipe-in [animation-duration:var(--duration-slower)] [--wipe-bleed:4px]", className),
2400
2667
  children: path && (variant === "area" ? /* @__PURE__ */ jsx("path", {
2401
2668
  d: path.area,
2402
- className: areaClass[tone]
2669
+ className: cn(areaClass[tone], areaForced)
2403
2670
  }) : /* @__PURE__ */ jsx("path", {
2404
2671
  d: path.line,
2405
2672
  fill: "none",
2406
2673
  strokeWidth: 1.5,
2407
2674
  strokeLinejoin: "miter",
2408
2675
  vectorEffect: "non-scaling-stroke",
2409
- className: lineClass[tone]
2676
+ className: cn(lineClass[tone], lineForced)
2410
2677
  }))
2411
2678
  });
2412
2679
  }
@@ -2480,7 +2747,7 @@ const toneClass$1 = {
2480
2747
  danger: "text-danger"
2481
2748
  };
2482
2749
  const favoriteDot = "block size-4 rounded-full";
2483
- const favoriteOn = "bg-action";
2750
+ const favoriteOn = "bg-action forced-colors:bg-[Highlight]";
2484
2751
  const favoriteOff = "border-2 border-ink-tertiary";
2485
2752
  /** 数据行带里的一行。放在 `DataRowList` 里。 */
2486
2753
  function DataRow({ name, categoryColor, favorite, onFavoriteChange, favoriteLabel = "收藏", series, value, tone = "neutral", reference, className, style, ...props }) {
@@ -2605,7 +2872,7 @@ function DateField({ display, placeholder, fields, variant, size, invalid, disab
2605
2872
  }),
2606
2873
  /* @__PURE__ */ jsx(CalendarIcon, {
2607
2874
  size: iconSize,
2608
- className: cn("shrink-0", disabled ? "text-ink-disabled" : "text-ink-secondary")
2875
+ className: cn("shrink-0", disabled ? "text-ink-disabled forced-colors:text-[GrayText]" : "text-ink-secondary forced-colors:text-[ButtonText]")
2609
2876
  })
2610
2877
  ]
2611
2878
  }), fields]
@@ -3229,12 +3496,12 @@ function Viewfinder$1() {
3229
3496
  });
3230
3497
  }
3231
3498
  /** 没有内容时的占位。说清楚为什么为空、接下来能做什么;不要用大幅插画填满。 */
3232
- function EmptyState({ title, description, action, icon, bordered = true, level = 3, className, children, ...props }) {
3499
+ function EmptyState({ title, description, action, icon, bordered = true, level = 3, animate = true, className, children, ...props }) {
3233
3500
  const Heading = `h${level}`;
3234
3501
  const graphic = icon === void 0 ? /* @__PURE__ */ jsx(Viewfinder$1, {}) : icon;
3235
3502
  return /* @__PURE__ */ jsxs("div", {
3236
3503
  ...props,
3237
- className: cn("flex flex-col items-center px-6 py-10 text-center text-ink", bordered && "border border-dashed border-line-strong", className),
3504
+ className: cn("flex flex-col items-center px-6 py-10 text-center text-ink", bordered && "border border-dashed border-line-strong", animate && "animate-fade-in", className),
3238
3505
  children: [
3239
3506
  graphic !== null && /* @__PURE__ */ jsx("div", {
3240
3507
  "aria-hidden": "true",
@@ -3314,7 +3581,8 @@ const trackSize = {
3314
3581
  sm: "h-1",
3315
3582
  md: "h-2"
3316
3583
  };
3317
- const fillClass = "bg-action";
3584
+ const fillClass = "bg-action forced-colors:bg-[Highlight]";
3585
+ const trackClass$1 = "bg-line forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[CanvasText]";
3318
3586
  /**
3319
3587
  * 进度:灰轨 + 黄填充。表示"完成了多少";表示"数量有多少"用图表,那是蓝色的事。
3320
3588
  *
@@ -3333,7 +3601,7 @@ function Progress({ value, max, segments, size = "md", showValue = false, format
3333
3601
  className: "flex h-full gap-1",
3334
3602
  children: Array.from({ length: segments }, (_, index) => /* @__PURE__ */ jsx("span", {
3335
3603
  "data-done": index < done ? "" : void 0,
3336
- className: cn("flex-1", index < done ? fillClass : "bg-line")
3604
+ className: cn("flex-1", index < done ? fillClass : trackClass$1)
3337
3605
  }, index))
3338
3606
  });
3339
3607
  } else if (indeterminate) bar = /* @__PURE__ */ jsx("span", { className: cn("absolute inset-y-0 left-0 w-2/5 animate-indeterminate", "motion-reduce:translate-x-3/4", fillClass) });
@@ -3358,7 +3626,7 @@ function Progress({ value, max, segments, size = "md", showValue = false, format
3358
3626
  "aria-valuenow": indeterminate ? void 0 : current,
3359
3627
  "aria-valuetext": segmented && !indeterminate ? `${current} / ${total}` : void 0,
3360
3628
  ...props,
3361
- className: cn("relative min-w-0 flex-1", trackSize[size], !segmented && "bg-line"),
3629
+ className: cn("relative min-w-0 flex-1", trackSize[size], !segmented && trackClass$1),
3362
3630
  children: bar
3363
3631
  }), text !== null && /* @__PURE__ */ jsx("span", {
3364
3632
  className: "min-w-[4ch] shrink-0 text-right font-tech text-sm leading-none font-bold tabular-nums",
@@ -3477,6 +3745,121 @@ function FileItem({ name, size, status, progress, error, onRemove, onRetry, remo
3477
3745
  });
3478
3746
  }
3479
3747
  //#endregion
3748
+ //#region src/hooks/useAddedKeys.ts
3749
+ const NONE = /* @__PURE__ */ new Set();
3750
+ const same$1 = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
3751
+ /**
3752
+ * 一串 key 里哪几个是后来加进来的。一开始就在的不算;删掉之后又加回来的算。
3753
+ *
3754
+ * 给"新项淡入"用:入场的类只加在这几个上。一开始就有的不该在载入时播一遍,
3755
+ * 已经在的也不会因为别的项变了而重播——它一直带着那个类,动画只在挂上时播一次。
3756
+ */
3757
+ function useAddedKeys(keys) {
3758
+ const [state, setState] = useState({
3759
+ keys,
3760
+ added: NONE
3761
+ });
3762
+ if (!same$1(state.keys, keys)) {
3763
+ const before = new Set(state.keys);
3764
+ setState({
3765
+ keys,
3766
+ added: new Set(keys.filter((key) => !before.has(key) || state.added.has(key)))
3767
+ });
3768
+ }
3769
+ return state.added;
3770
+ }
3771
+ //#endregion
3772
+ //#region src/hooks/useLingering.ts
3773
+ const same = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
3774
+ /**
3775
+ * 新的一份:现在的那几项按现在的顺序,走着的插回它原来挨着的那一项后面。
3776
+ * `keep` 是这一次新走的要不要留。
3777
+ */
3778
+ function merge(before, keys, keep) {
3779
+ const now = new Set(keys);
3780
+ const next = keys.map((key) => ({
3781
+ key,
3782
+ leaving: false
3783
+ }));
3784
+ let anchor = null;
3785
+ for (const entry of before) {
3786
+ if (now.has(entry.key)) {
3787
+ anchor = entry.key;
3788
+ continue;
3789
+ }
3790
+ if (!entry.leaving && !keep) continue;
3791
+ const at = anchor === null ? -1 : next.findIndex((item) => item.key === anchor);
3792
+ next.splice(at + 1, 0, {
3793
+ key: entry.key,
3794
+ leaving: true
3795
+ });
3796
+ anchor = entry.key;
3797
+ }
3798
+ return next;
3799
+ }
3800
+ /**
3801
+ * 列表里被拿掉的项先留着:`keys` 里少了一项,它不当场消失,而是带着 `leaving`
3802
+ * 留在原来的位置上,等它身上的退场走完才真的拿掉(等法和 `usePresence` 是同一套:
3803
+ * 看元素上正在跑的动效,一个都没有就当场拿掉——测试环境里东西删了就是删了)。
3804
+ *
3805
+ * 用法:`entries` 按顺序渲染成 `ref` 那个容器的直接子元素,走着的那几项带上
3806
+ * `data-leaving`(退场的样式挂在它上面),并且对键盘和读屏隐藏。钩子按这个标记找元素。
3807
+ *
3808
+ * - 这一次变化里同时有加进来的(整批换掉、"只收一个"换了一个):新走的不留,照旧直接换。
3809
+ * - 走到一半又加回来:它不再是走着的,留在页面上的还是同一个元素。
3810
+ * - `enabled` 为假时不留新走的(项太多的时候)。
3811
+ */
3812
+ function useLingering(keys, enabled = true) {
3813
+ const ref = useRef(null);
3814
+ const [state, setState] = useState(() => ({
3815
+ keys,
3816
+ entries: merge([], keys, false)
3817
+ }));
3818
+ let { entries } = state;
3819
+ if (!same(state.keys, keys)) {
3820
+ const known = new Set(state.keys);
3821
+ const grew = keys.some((key) => !known.has(key));
3822
+ entries = merge(state.entries, keys, enabled && !grew);
3823
+ setState({
3824
+ keys,
3825
+ entries
3826
+ });
3827
+ }
3828
+ const drop = useCallback((key) => {
3829
+ setState((current) => ({
3830
+ keys: current.keys,
3831
+ entries: current.entries.filter((entry) => !(entry.leaving && entry.key === key))
3832
+ }));
3833
+ }, []);
3834
+ const waits = useRef(/* @__PURE__ */ new Map());
3835
+ useLayoutEffect(() => {
3836
+ const pending = waits.current;
3837
+ const leaving = entries.filter((entry) => entry.leaving).map((entry) => entry.key);
3838
+ for (const [key, cancel] of pending) {
3839
+ if (leaving.includes(key)) continue;
3840
+ cancel();
3841
+ pending.delete(key);
3842
+ }
3843
+ if (leaving.length === 0) return;
3844
+ const nodes = [...ref.current?.children ?? []].filter((node) => node.hasAttribute("data-leaving"));
3845
+ leaving.forEach((key, index) => {
3846
+ if (pending.has(key)) return;
3847
+ pending.set(key, afterMotion(nodes[index] ?? null, () => drop(key)));
3848
+ });
3849
+ }, [entries, drop]);
3850
+ useEffect(() => {
3851
+ const pending = waits.current;
3852
+ return () => {
3853
+ for (const cancel of pending.values()) cancel();
3854
+ pending.clear();
3855
+ };
3856
+ }, []);
3857
+ return {
3858
+ ref,
3859
+ entries
3860
+ };
3861
+ }
3862
+ //#endregion
3480
3863
  //#region src/icons/Upload.tsx
3481
3864
  /** 上传:一个托盘,上面一支朝上的箭头 */
3482
3865
  const Upload = createIcon("Upload", /* @__PURE__ */ jsx("path", { d: "M12 16V4M7 9l5-5 5 5M4 15v5h16v-5" }));
@@ -3494,6 +3877,8 @@ const zoneSize = {
3494
3877
  sm: "min-h-10 flex-wrap gap-x-3 px-3 py-2 text-sm",
3495
3878
  md: "gap-3 px-4 py-4"
3496
3879
  };
3880
+ /** 列表里认一个文件靠的那串字 */
3881
+ const fileKey = (file) => `${file.name}-${file.size}-${file.lastModified}`;
3497
3882
  /** 拖进来的东西里的文件;文件夹不收 */
3498
3883
  function droppedFiles(transfer) {
3499
3884
  const items = [...transfer.items ?? []];
@@ -3505,6 +3890,25 @@ function droppedFiles(transfer) {
3505
3890
  });
3506
3891
  }
3507
3892
  /**
3893
+ * 列表里的一行。淡入、收起都写在这一层:里面那一行可能是使用方自己画的(renderFile)。
3894
+ * 移除的那一行先留着——高度收到 0 并淡出,连它占的 4px 行距一起收,收完才卸载;
3895
+ * 下面的行和控件后面的内容是被带着上来的。
3896
+ */
3897
+ function FileRow({ leaving, entering, children }) {
3898
+ const [last, setLast] = useState(children);
3899
+ if (!leaving && last !== children) setLast(children);
3900
+ return /* @__PURE__ */ jsx("li", {
3901
+ "data-leaving": leaving ? "" : void 0,
3902
+ inert: leaving,
3903
+ "aria-hidden": leaving || void 0,
3904
+ className: cn(collapse, "transition-[grid-template-rows,margin,opacity] duration-(--duration-fast)", "data-leaving:-mb-1 data-leaving:opacity-0", entering && "animate-fade-in [animation-duration:var(--duration-fast)]"),
3905
+ children: /* @__PURE__ */ jsx("div", {
3906
+ className: "min-h-0 min-w-0 in-data-leaving:overflow-y-clip",
3907
+ children: leaving ? last : children
3908
+ })
3909
+ });
3910
+ }
3911
+ /**
3508
3912
  * 文件上传:选文件,或者把文件拖进来。它只管"选"和"列"——发请求、算进度是使用方的事。
3509
3913
  *
3510
3914
  * 里面是一个真的 `<input type="file">`,铺满整个投放区:点哪儿都是打开系统的文件框。
@@ -3529,6 +3933,9 @@ function FileUpload({ value, defaultValue, onValueChange, multiple = false, acce
3529
3933
  defaultValue: defaultValue ?? [],
3530
3934
  onChange: onValueChange
3531
3935
  });
3936
+ const keys = files.map(fileKey);
3937
+ const added = useAddedKeys(keys);
3938
+ const lingering = useLingering(keys);
3532
3939
  const [rejections, setRejections] = useState([]);
3533
3940
  const [dragging, setDragging] = useState(false);
3534
3941
  const depth = useRef(0);
@@ -3547,7 +3954,7 @@ function FileUpload({ value, defaultValue, onValueChange, multiple = false, acce
3547
3954
  const index = pendingFocus.current;
3548
3955
  if (index === null) return;
3549
3956
  pendingFocus.current = null;
3550
- const buttons = listRef.current?.querySelectorAll("[data-file-remove]") ?? [];
3957
+ const buttons = listRef.current?.querySelectorAll("li:not([data-leaving]) [data-file-remove]") ?? [];
3551
3958
  const target = buttons[Math.min(index, buttons.length - 1)];
3552
3959
  if (target) target.focus();
3553
3960
  else inputRef.current?.focus();
@@ -3683,22 +4090,29 @@ function FileUpload({ value, defaultValue, onValueChange, multiple = false, acce
3683
4090
  })]
3684
4091
  })
3685
4092
  }),
3686
- files.length > 0 && /* @__PURE__ */ jsx("ul", {
3687
- ref: listRef,
4093
+ lingering.entries.length > 0 && /* @__PURE__ */ jsx("ul", {
4094
+ ref: mergeRefs(listRef, lingering.ref),
3688
4095
  "aria-label": labels.list,
3689
- className: "flex flex-col gap-1",
3690
- children: files.map((file, index) => {
4096
+ "data-emptying": files.length === 0 ? "" : void 0,
4097
+ className: "flex flex-col gap-1 transition-[margin] duration-(--duration-fast) ease-standard data-emptying:-mt-2",
4098
+ children: lingering.entries.map(({ key, leaving }) => {
4099
+ const index = keys.indexOf(key);
4100
+ const file = files[index];
3691
4101
  const actions = {
3692
4102
  remove: () => remove(index),
3693
4103
  index
3694
4104
  };
3695
- return /* @__PURE__ */ jsx("li", { children: renderFile ? renderFile(file, actions) : /* @__PURE__ */ jsx(FileItem, {
3696
- name: file.name,
3697
- size: file.size,
3698
- disabled,
3699
- removeLabel: removeLabel(file.name),
3700
- onRemove: actions.remove
3701
- }) }, `${file.name}-${file.size}-${file.lastModified}`);
4105
+ return /* @__PURE__ */ jsx(FileRow, {
4106
+ leaving,
4107
+ entering: added.has(key),
4108
+ children: file !== void 0 && (renderFile ? renderFile(file, actions) : /* @__PURE__ */ jsx(FileItem, {
4109
+ name: file.name,
4110
+ size: file.size,
4111
+ disabled,
4112
+ removeLabel: removeLabel(file.name),
4113
+ onRemove: actions.remove
4114
+ }))
4115
+ }, key);
3702
4116
  })
3703
4117
  }),
3704
4118
  /* @__PURE__ */ jsx("span", {
@@ -4574,23 +4988,8 @@ function ListRow({ start, end, description, selected, completed = false, complet
4574
4988
  * 加载页:近黑底、大号等宽百分比、一行标语、黄色进度条。只用于整页的首次加载。
4575
4989
  * 不伪造进度;内容就绪后立刻把 `open` 置为 `false`,不要为了播完动画而等待。
4576
4990
  */
4577
- function Loader({ value, tagline, open = true, fullscreen = true, label = "加载中", onExited, onKeyDown, onTransitionEnd, className, ...props }) {
4578
- const rootRef = useRef(null);
4579
- const [mounted, setMounted] = useState(open);
4580
- if (open && !mounted) setMounted(true);
4581
- const onExitedRef = useRef(onExited);
4582
- useEffect(() => {
4583
- onExitedRef.current = onExited;
4584
- });
4585
- const leaving = !open && mounted;
4586
- useEffect(() => {
4587
- if (!leaving) return;
4588
- const timer = window.setTimeout(() => {
4589
- setMounted(false);
4590
- onExitedRef.current?.();
4591
- }, 700);
4592
- return () => window.clearTimeout(timer);
4593
- }, [leaving]);
4991
+ function Loader({ value, tagline, open = true, fullscreen = true, label = "加载中", onExited, onKeyDown, className, ref, ...props }) {
4992
+ const { ref: rootRef, mounted } = usePresence(open, { onExited });
4594
4993
  useEffect(() => {
4595
4994
  if (!fullscreen || !open) return;
4596
4995
  const page = document.documentElement;
@@ -4602,15 +5001,12 @@ function Loader({ value, tagline, open = true, fullscreen = true, label = "加
4602
5001
  page.style.overflow = previousOverflow;
4603
5002
  if (previousFocus instanceof HTMLElement && previousFocus.isConnected) previousFocus.focus({ preventScroll: true });
4604
5003
  };
4605
- }, [fullscreen, open]);
5004
+ }, [
5005
+ fullscreen,
5006
+ open,
5007
+ rootRef
5008
+ ]);
4606
5009
  if (!mounted) return null;
4607
- const handleTransitionEnd = (event) => {
4608
- onTransitionEnd?.(event);
4609
- if (leaving && event.target === event.currentTarget) {
4610
- setMounted(false);
4611
- onExitedRef.current?.();
4612
- }
4613
- };
4614
5010
  const handleKeyDown = (event) => {
4615
5011
  onKeyDown?.(event);
4616
5012
  if (fullscreen && open && event.key === "Tab") event.preventDefault();
@@ -4618,12 +5014,11 @@ function Loader({ value, tagline, open = true, fullscreen = true, label = "加
4618
5014
  const indeterminate = value === void 0;
4619
5015
  const current = indeterminate ? 0 : Math.min(Math.max(value, 0), 100);
4620
5016
  return /* @__PURE__ */ jsx("div", {
4621
- ref: rootRef,
4622
5017
  tabIndex: -1,
4623
5018
  ...props,
5019
+ ref: mergeRefs(rootRef, ref),
4624
5020
  "data-state": open ? "open" : "closing",
4625
5021
  onKeyDown: handleKeyDown,
4626
- onTransitionEnd: handleTransitionEnd,
4627
5022
  className: cn("@container flex flex-col justify-end bg-neutral-950 text-neutral-0 outline-none", fullscreen ? "fixed inset-0 z-(--z-loader)" : "absolute inset-0", "transition-[translate] duration-(--duration-slower) ease-emphasis", open ? "translate-y-0" : "pointer-events-none -translate-y-full", className),
4628
5023
  children: /* @__PURE__ */ jsxs("div", {
4629
5024
  className: "flex flex-col gap-4 p-6 @md:p-10",
@@ -4649,9 +5044,9 @@ function Loader({ value, tagline, open = true, fullscreen = true, label = "加
4649
5044
  "aria-valuemin": 0,
4650
5045
  "aria-valuemax": 100,
4651
5046
  "aria-valuenow": indeterminate ? void 0 : current,
4652
- className: "relative h-1.5 overflow-hidden rounded-sm bg-neutral-800",
4653
- children: indeterminate ? /* @__PURE__ */ jsx("span", { className: "absolute inset-y-0 left-0 w-2/5 animate-indeterminate bg-action motion-reduce:translate-x-3/4" }) : /* @__PURE__ */ jsx("span", {
4654
- className: "absolute inset-0 origin-left bg-action transition-transform duration-(--duration-base) ease-standard",
5047
+ className: "relative h-1.5 overflow-hidden rounded-sm bg-neutral-800 forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[CanvasText]",
5048
+ children: indeterminate ? /* @__PURE__ */ jsx("span", { className: "absolute inset-y-0 left-0 w-2/5 animate-indeterminate bg-action motion-reduce:translate-x-3/4 forced-colors:bg-[Highlight]" }) : /* @__PURE__ */ jsx("span", {
5049
+ className: "absolute inset-0 origin-left bg-action transition-transform duration-(--duration-base) ease-standard forced-colors:bg-[Highlight]",
4655
5050
  style: { transform: `scaleX(${current / 100})` }
4656
5051
  })
4657
5052
  })
@@ -4963,7 +5358,7 @@ function NavMenuItem({ icon, current = false, disabled = false, closeOnClick = t
4963
5358
  const classes = cn("relative flex h-14 w-full items-center gap-3 px-4 text-left text-base font-medium", "transition-colors duration-(--duration-fast) ease-standard", disabled ? "cursor-not-allowed bg-surface-sunken text-ink-disabled" : current ? [
4964
5359
  "bg-action font-bold text-on-action",
4965
5360
  "before:absolute before:inset-y-0 before:left-0 before:w-1 before:bg-on-action before:content-['']",
4966
- "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-on-action"
5361
+ "focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-on-action"
4967
5362
  ] : ["bg-surface-sunken text-ink hover:bg-surface-muted active:bg-line", focusRingInset]);
4968
5363
  const mark = disabled ? "text-ink-disabled" : current ? "text-on-action" : "text-ink-secondary";
4969
5364
  const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
@@ -5125,7 +5520,7 @@ function OtpInput({ length = 6, value, defaultValue, onValueChange, onComplete,
5125
5520
  const labelId = useId();
5126
5521
  const ownLabel = ariaLabelledBy ?? (ariaLabel ? labelId : void 0);
5127
5522
  const groupLabel = ownLabel ?? fieldContext?.labelId;
5128
- const slotClass = cn("min-w-0 shrink text-center font-tech font-medium tabular-nums text-transparent caret-ink", "transition-colors duration-(--duration-fast) ease-standard", disabled ? "data-filled:text-ink-disabled" : "data-filled:text-ink", focusRing, slotSize[size], slotFrame[variant], readOnly ? "border-transparent bg-transparent" : slotFill[variant], disabled ? "cursor-not-allowed border-line" : invalid ? "border-danger" : !readOnly && "border-line-strong hover:not-focus:border-ink-secondary focus:border-ink");
5523
+ const slotClass = cn("min-w-0 shrink text-center font-tech font-medium tabular-nums text-transparent caret-ink", "transition-colors duration-(--duration-fast) ease-standard", disabled ? "data-filled:text-ink-disabled" : "data-filled:text-ink", focusRing, slotSize[size], slotFrame[variant], readOnly ? "border-transparent bg-transparent" : slotFill[variant], readOnly && !disabled && !invalid && "forced-colors:border-[Canvas]", disabled ? "cursor-not-allowed border-line" : invalid ? "border-danger" : !readOnly && "border-line-strong hover:not-focus:border-ink-secondary focus:border-ink");
5129
5524
  const group = groupSize !== void 0 && groupSize > 0 ? groupSize : 0;
5130
5525
  return /* @__PURE__ */ jsxs(OTPField.Root, {
5131
5526
  ...props,
@@ -5157,7 +5552,7 @@ function OtpInput({ length = 6, value, defaultValue, onValueChange, onComplete,
5157
5552
  }), Array.from({ length }, (_, index) => /* @__PURE__ */ jsxs(Fragment, { children: [group > 0 && index > 0 && index % group === 0 && /* @__PURE__ */ jsx("span", {
5158
5553
  "aria-hidden": "true",
5159
5554
  "data-separator": "",
5160
- className: "h-0.5 w-2 shrink-0 bg-line-strong"
5555
+ className: "h-0.5 w-2 shrink-0 bg-line-strong forced-colors:bg-[CanvasText]"
5161
5556
  }), /* @__PURE__ */ jsx(OTPField.Input, {
5162
5557
  ...index === 0 ? {
5163
5558
  "aria-labelledby": ownLabel,
@@ -5175,12 +5570,20 @@ function OtpInput({ length = 6, value, defaultValue, onValueChange, onComplete,
5175
5570
  const ArrowLeft = createIcon("ArrowLeft", /* @__PURE__ */ jsx("path", { d: "M20 12H5M11 5l-7 7 7 7" }));
5176
5571
  //#endregion
5177
5572
  //#region src/components/page-header/PageHeader.tsx
5573
+ const enter = "animate-shift-in [--shift-x:calc(var(--motion-shift-lg)*-1)]";
5574
+ const enterStep = [
5575
+ enter,
5576
+ `${enter} [animation-delay:100ms]`,
5577
+ `${enter} [animation-delay:200ms]`
5578
+ ];
5178
5579
  /**
5179
5580
  * 页头:这是哪一页、怎么回去、在这一页能做什么。一页一个。
5180
5581
  * 按自身的宽度响应:行动区放不下时折到标题下面。
5181
5582
  */
5182
- function PageHeader({ title, level = 1, meta, description, breadcrumb, back, actions, divider = false, sticky = false, className, children, ...props }) {
5583
+ function PageHeader({ title, level = 1, meta, description, breadcrumb, back, actions, divider = false, sticky = false, animate = true, className, children, ...props }) {
5183
5584
  const Heading = `h${level}`;
5585
+ const reveal = animate && !sticky;
5586
+ const first = meta ? 1 : 0;
5184
5587
  return /* @__PURE__ */ jsxs("header", {
5185
5588
  ...props,
5186
5589
  "data-sticky": sticky ? "" : void 0,
@@ -5190,7 +5593,7 @@ function PageHeader({ title, level = 1, meta, description, breadcrumb, back, act
5190
5593
  /* @__PURE__ */ jsxs("div", {
5191
5594
  className: "flex flex-col gap-1",
5192
5595
  children: [meta && /* @__PURE__ */ jsx("p", {
5193
- className: "font-tech text-xs text-ink-secondary",
5596
+ className: cn("font-tech text-xs text-ink-secondary", reveal && enterStep[0]),
5194
5597
  children: meta
5195
5598
  }), /* @__PURE__ */ jsxs("div", {
5196
5599
  className: "flex flex-wrap items-start justify-between gap-x-6 gap-y-4",
@@ -5202,10 +5605,10 @@ function PageHeader({ title, level = 1, meta, description, breadcrumb, back, act
5202
5605
  }), /* @__PURE__ */ jsxs("div", {
5203
5606
  className: "flex min-w-0 flex-col gap-1",
5204
5607
  children: [/* @__PURE__ */ jsx(Heading, {
5205
- className: "min-w-0 text-2xl leading-10 font-bold wrap-anywhere @md:text-3xl @md:leading-12",
5608
+ className: cn("min-w-0 text-2xl leading-10 font-bold wrap-anywhere @md:text-3xl @md:leading-12", reveal && enterStep[first]),
5206
5609
  children: title
5207
5610
  }), description && /* @__PURE__ */ jsx("p", {
5208
- className: "max-w-prose text-sm text-ink-secondary",
5611
+ className: cn("max-w-prose text-sm text-ink-secondary", reveal && enterStep[first + 1]),
5209
5612
  children: description
5210
5613
  })]
5211
5614
  })]
@@ -5299,7 +5702,7 @@ function Pagination({ page, defaultPage = 1, onPageChange, pageCount, jump = fal
5299
5702
  "aria-label": "分页",
5300
5703
  ...props,
5301
5704
  "data-size": size,
5302
- className: cn("@container hatch flex items-center justify-between gap-2 bg-surface-muted px-1 text-ink", barSize[size], className),
5705
+ className: cn("@container hatch flex items-center justify-between gap-2 bg-surface-muted px-1 text-ink", "forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[ButtonText]", barSize[size], className),
5303
5706
  children: [
5304
5707
  /* @__PURE__ */ jsx(IconButton, {
5305
5708
  variant: "accent",
@@ -5458,6 +5861,8 @@ function PopoverClose({ children }) {
5458
5861
  }
5459
5862
  //#endregion
5460
5863
  //#region src/components/progress/ProgressRing.tsx
5864
+ const trackClass = "stroke-line forced-colors:stroke-[CanvasText] forced-colors:stroke-1";
5865
+ const arcClass = "stroke-action forced-colors:stroke-[Highlight]";
5461
5866
  /**
5462
5867
  * 进度环:细圆环轨道 + 从 12 点方向顺时针的黄弧。用在紧凑的位置,或者围住一个头像、图标。
5463
5868
  * 取色与 `Progress` 相同;需要名称时传 `aria-label`。
@@ -5479,16 +5884,16 @@ function ProgressRing({ value, max = 100, size = 48, thickness, showValue = fals
5479
5884
  let arc = null;
5480
5885
  if (indeterminate) arc = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("circle", {
5481
5886
  ...circle,
5482
- className: "origin-center animate-spin stroke-action motion-reduce:hidden",
5887
+ className: cn("origin-center animate-spin motion-reduce:hidden", arcClass),
5483
5888
  strokeDasharray: `${circumference / 4} ${circumference}`
5484
5889
  }), /* @__PURE__ */ jsx("circle", {
5485
5890
  ...circle,
5486
- className: "hidden stroke-action motion-reduce:block",
5891
+ className: cn("hidden motion-reduce:block", arcClass),
5487
5892
  strokeDasharray: `${stroke} ${stroke * 2}`
5488
5893
  })] });
5489
5894
  else if (ratio > 0) arc = /* @__PURE__ */ jsx("circle", {
5490
5895
  ...circle,
5491
- className: "stroke-action transition-[stroke-dashoffset] duration-(--duration-base) ease-standard",
5896
+ className: cn("transition-[stroke-dashoffset] duration-(--duration-base) ease-standard", arcClass),
5492
5897
  strokeDasharray: circumference,
5493
5898
  strokeDashoffset: circumference * (1 - ratio)
5494
5899
  });
@@ -5522,7 +5927,7 @@ function ProgressRing({ value, max = 100, size = 48, thickness, showValue = fals
5522
5927
  className: "absolute inset-0 -rotate-90",
5523
5928
  children: [/* @__PURE__ */ jsx("circle", {
5524
5929
  ...circle,
5525
- className: "stroke-line"
5930
+ className: trackClass
5526
5931
  }), arc]
5527
5932
  }), center]
5528
5933
  });
@@ -5610,7 +6015,7 @@ function Radio({ value, disabled, onChange, className, children, ...props }) {
5610
6015
  className: cn(choiceBox, "rounded-full choice-diamond:rounded-none", focusRing)
5611
6016
  }), /* @__PURE__ */ jsx("span", {
5612
6017
  "aria-hidden": "true",
5613
- className: cn(choiceMark, "size-2 rounded-full bg-accent-ink-inverse peer-checked:opacity-100 peer-disabled:bg-ink-disabled choice-diamond:hidden")
6018
+ className: cn(choiceMark, "size-2 rounded-full bg-accent-ink-inverse peer-checked:opacity-100 peer-disabled:bg-ink-disabled choice-diamond:hidden", "forced-colors:bg-[HighlightText] forced-colors:peer-disabled:bg-[GrayText]")
5614
6019
  })]
5615
6020
  })
5616
6021
  }), children !== void 0 && /* @__PURE__ */ jsx("span", {
@@ -5671,6 +6076,105 @@ function RegistrationStrip({ orientation = "horizontal", rule = false, className
5671
6076
  });
5672
6077
  }
5673
6078
  //#endregion
6079
+ //#region src/hooks/useReducedMotion.ts
6080
+ const QUERY = "(prefers-reduced-motion: reduce)";
6081
+ function subscribe$1(notify) {
6082
+ if (typeof window.matchMedia !== "function") return () => {};
6083
+ const media = window.matchMedia(QUERY);
6084
+ media.addEventListener("change", notify);
6085
+ return () => media.removeEventListener("change", notify);
6086
+ }
6087
+ function read() {
6088
+ return typeof window.matchMedia === "function" && window.matchMedia(QUERY).matches;
6089
+ }
6090
+ /**
6091
+ * 系统开了"减少动态效果"时返回 true,设置变了会跟着变。
6092
+ *
6093
+ * CSS 的动画和过渡不用它:`theme.css` 末尾的全局规则已经统一降级了。
6094
+ * 只有脚本驱动的动效(一帧一帧自己算的)才要看它——那条规则管不到脚本。
6095
+ */
6096
+ function useReducedMotion() {
6097
+ return useSyncExternalStore(subscribe$1, read, () => false);
6098
+ }
6099
+ //#endregion
6100
+ //#region src/components/rolling-number/RollingNumber.tsx
6101
+ const FALLBACK_DURATION = 600;
6102
+ function readDuration(node) {
6103
+ if (!node) return FALLBACK_DURATION;
6104
+ const raw = getComputedStyle(node).getPropertyValue("--duration-slower");
6105
+ const amount = Number.parseFloat(raw);
6106
+ if (!Number.isFinite(amount) || amount <= 0) return FALLBACK_DURATION;
6107
+ return raw.trim().endsWith("ms") ? amount : amount * 1e3;
6108
+ }
6109
+ /** 减速:先快后慢,停得住。和 `ease-exit` 是一类,不带过冲 */
6110
+ const decelerate = (progress) => 1 - (1 - progress) ** 3;
6111
+ function fractionDigits(value) {
6112
+ const fraction = new Intl.NumberFormat("en-US", {
6113
+ maximumFractionDigits: 20,
6114
+ useGrouping: false
6115
+ }).format(value).split(".")[1];
6116
+ return fraction ? fraction.length : 0;
6117
+ }
6118
+ const never = () => () => {};
6119
+ /**
6120
+ * 滚动数字:进入视口时从 0 滚到终值,只滚一次。放进 `Stat` 的 `value` 里,
6121
+ * 或者任何"一个需要被一眼看到的数字"的地方。等宽数字由所在处给(`tabular-nums`)。
6122
+ *
6123
+ * - 读屏只读终值:终值一直在,滚的时候只是看不见;滚动的那一份对读屏隐藏。
6124
+ * - 宽度由终值占着,滚的时候旁边的东西不动。
6125
+ * - 服务端渲染出来的、打印出来的、"减少动态效果"下的,都直接是终值。
6126
+ * - 值后来变了直接换,不再滚。
6127
+ */
6128
+ function RollingNumber({ value, format, animate = true, className, ref, ...props }) {
6129
+ const digits = fractionDigits(value);
6130
+ const write = format ?? ((amount) => amount.toLocaleString("en-US", {
6131
+ minimumFractionDigits: digits,
6132
+ maximumFractionDigits: digits
6133
+ }));
6134
+ const mounted = useSyncExternalStore(never, () => true, () => false);
6135
+ const reduced = useReducedMotion();
6136
+ const [done, setDone] = useState(false);
6137
+ const [shown, setShown] = useState(0);
6138
+ const rolling = mounted && animate && !reduced && !done;
6139
+ const [inViewRef, inView] = useInView({ disabled: !rolling });
6140
+ useEffect(() => {
6141
+ if (!rolling || !inView) return;
6142
+ const duration = readDuration(inViewRef.current);
6143
+ let started;
6144
+ let frame = requestAnimationFrame(function tick(now) {
6145
+ started ??= now;
6146
+ const progress = Math.min((now - started) / duration, 1);
6147
+ if (progress >= 1) {
6148
+ setDone(true);
6149
+ return;
6150
+ }
6151
+ setShown(Number((value * decelerate(progress)).toFixed(digits)));
6152
+ frame = requestAnimationFrame(tick);
6153
+ });
6154
+ return () => cancelAnimationFrame(frame);
6155
+ }, [
6156
+ rolling,
6157
+ inView,
6158
+ inViewRef,
6159
+ value,
6160
+ digits
6161
+ ]);
6162
+ return /* @__PURE__ */ jsxs("span", {
6163
+ ...props,
6164
+ ref: mergeRefs(inViewRef, ref),
6165
+ "data-rolling": rolling ? "" : void 0,
6166
+ className: cn(rolling && "relative inline-block", className),
6167
+ children: [/* @__PURE__ */ jsx("span", {
6168
+ className: rolling ? "opacity-0 print:opacity-100" : void 0,
6169
+ children: write(value)
6170
+ }), rolling && /* @__PURE__ */ jsx("span", {
6171
+ "aria-hidden": "true",
6172
+ className: "absolute inset-0 print:hidden",
6173
+ children: write(shown)
6174
+ })]
6175
+ });
6176
+ }
6177
+ //#endregion
5674
6178
  //#region src/hooks/useOverflowing.ts
5675
6179
  /**
5676
6180
  * 容器里的内容是不是比容器宽、比容器高。
@@ -5806,11 +6310,11 @@ function Schedule({ label, start, end, anchors: anchorsProp, today, dayWidth = 3
5806
6310
  }),
5807
6311
  todayColumn !== null && /* @__PURE__ */ jsx("div", {
5808
6312
  "aria-hidden": "true",
5809
- className: cn("pointer-events-none select-none forced-colors:hidden print:hidden", "absolute inset-y-0 right-0 z-2", AFTER_RAIL, dayColumns),
6313
+ className: cn("pointer-events-none absolute inset-y-0 right-0 z-2 select-none print:hidden", AFTER_RAIL, dayColumns),
5810
6314
  children: /* @__PURE__ */ jsx("span", {
5811
6315
  "data-today": "",
5812
6316
  style: { gridColumn: todayColumn },
5813
- className: "row-start-1 border-l-2 border-accent-ink"
6317
+ className: "row-start-1 border-l-2 border-accent-ink forced-colors:border-[Highlight]"
5814
6318
  })
5815
6319
  })
5816
6320
  ]
@@ -5872,7 +6376,7 @@ function ScheduleItem({ start, end, title, type, variant = "banner", media, href
5872
6376
  gridColumn: `${from} / ${to}`,
5873
6377
  ...style
5874
6378
  },
5875
- className: cn("@container relative isolate flex min-w-0 overflow-clip text-sm", "has-focus-visible:outline-2 has-focus-visible:-outline-offset-2 has-focus-visible:outline-focus", banner ? "items-start justify-between gap-2 bg-surface-muted p-2" : "items-center gap-2 bg-control pr-2 text-on-control", className),
6379
+ className: cn("@container relative isolate flex min-w-0 overflow-clip text-sm", "has-focus-visible:outline-2 has-focus-visible:-outline-offset-2 has-focus-visible:outline-focus", "forced-colors:border forced-colors:border-[CanvasText]", banner ? "items-start justify-between gap-2 bg-surface-muted p-2" : "items-center gap-2 bg-control pr-2 text-on-control", className),
5876
6380
  children: [banner ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
5877
6381
  media && /* @__PURE__ */ jsx("div", {
5878
6382
  "aria-hidden": "true",
@@ -5995,40 +6499,6 @@ function ScrollHint({ label = "SCROLL", variant = "line", target, threshold = 8,
5995
6499
  });
5996
6500
  }
5997
6501
  //#endregion
5998
- //#region src/hooks/useInView.ts
5999
- /**
6000
- * 元素第一次进入视口后返回 true,之后不再变回 false——入场动画只播一次。
6001
- */
6002
- function useInView({ disabled = false, rootMargin = "0px", threshold = 0 } = {}) {
6003
- const ref = useRef(null);
6004
- const [inView, setInView] = useState(false);
6005
- useEffect(() => {
6006
- const node = ref.current;
6007
- if (disabled || inView || !node) return;
6008
- if (typeof IntersectionObserver === "undefined") {
6009
- setInView(true);
6010
- return;
6011
- }
6012
- const observer = new IntersectionObserver((entries) => {
6013
- if (entries.some((entry) => entry.isIntersecting)) {
6014
- setInView(true);
6015
- observer.disconnect();
6016
- }
6017
- }, {
6018
- rootMargin,
6019
- threshold
6020
- });
6021
- observer.observe(node);
6022
- return () => observer.disconnect();
6023
- }, [
6024
- disabled,
6025
- inView,
6026
- rootMargin,
6027
- threshold
6028
- ]);
6029
- return [ref, inView];
6030
- }
6031
- //#endregion
6032
6502
  //#region src/components/section-title/SectionTitle.tsx
6033
6503
  function SectionTitle({ variant = "standard", children, latin, subtitle, meta, level = 2, animate = true, latinHidden = true, tone = "accent", illustration, className, ref: forwardedRef, ...props }) {
6034
6504
  const Heading = `h${level}`;
@@ -6131,6 +6601,105 @@ function SectionTitle({ variant = "standard", children, latin, subtitle, meta, l
6131
6601
  });
6132
6602
  }
6133
6603
  //#endregion
6604
+ //#region src/hooks/useIndicator.ts
6605
+ const VARS = [
6606
+ "--indicator-x",
6607
+ "--indicator-y",
6608
+ "--indicator-w",
6609
+ "--indicator-h"
6610
+ ];
6611
+ function clear(node) {
6612
+ if (node.dataset.indicator === void 0) return;
6613
+ delete node.dataset.indicator;
6614
+ for (const name of VARS) node.style.removeProperty(name);
6615
+ }
6616
+ /** 量一次、写一次。`last` 记着上一次指的是哪一项:换了才滑 */
6617
+ function place(node, selector, last) {
6618
+ const box = node.getBoundingClientRect();
6619
+ if (box.width === 0 || box.height === 0) {
6620
+ clear(node);
6621
+ last.current = null;
6622
+ return;
6623
+ }
6624
+ const item = node.querySelector(selector);
6625
+ const rect = item?.getBoundingClientRect();
6626
+ if (!item || !rect || rect.width === 0 || rect.height === 0) {
6627
+ node.dataset.indicator = "off";
6628
+ last.current = null;
6629
+ return;
6630
+ }
6631
+ const laid = Number.parseFloat(getComputedStyle(node).width);
6632
+ const scale = laid > 0 ? box.width / laid : 1;
6633
+ const values = [
6634
+ (rect.left - box.left) / scale - node.clientLeft + node.scrollLeft,
6635
+ (rect.top - box.top) / scale - node.clientTop + node.scrollTop,
6636
+ rect.width / scale,
6637
+ rect.height / scale
6638
+ ].map((value) => `${Math.round(value * 100) / 100}px`);
6639
+ const slide = node.dataset.indicator === "on" && last.current !== null && last.current !== item;
6640
+ last.current = item;
6641
+ if (VARS.some((name, at) => node.style.getPropertyValue(name) !== values[at])) {
6642
+ if (!slide) node.dataset.indicatorInstant = "";
6643
+ VARS.forEach((name, at) => node.style.setProperty(name, values[at]));
6644
+ if (!slide) {
6645
+ node.getBoundingClientRect();
6646
+ delete node.dataset.indicatorInstant;
6647
+ }
6648
+ }
6649
+ node.dataset.indicator = "on";
6650
+ }
6651
+ /**
6652
+ * 选中指示的位置:量出容器里"当前项"相对容器的位置和尺寸,写成容器上的 CSS 变量
6653
+ * (`--indicator-x` / `-y` / `-w` / `-h`),并在容器上标 `data-indicator`——`on` 是有当前项,
6654
+ * `off` 是没有。指示本身由用的地方画在容器的 `::after` 上(`lib/motion.ts` 的 `indicator`),
6655
+ * 换了一项时由 CSS 的过渡滑过去。
6656
+ *
6657
+ * 只有"换了一项"才滑;第一次到位、同一项挪了地方或变了大小(容器宽了、字体到了、
6658
+ * 前面多了一项)都直接到位。量不到(服务端、还没排版、藏着)时什么都不标,
6659
+ * 各项照旧自己画选中态——不会有哪一帧没有选中态。
6660
+ *
6661
+ * `current` 是当前项在容器里的选择器;传 `null` 是不用。
6662
+ */
6663
+ function useIndicator(current) {
6664
+ const ref = useRef(null);
6665
+ const last = useRef(null);
6666
+ const observer = useRef(null);
6667
+ const watched = useRef([]);
6668
+ useLayoutEffect(() => {
6669
+ const node = ref.current;
6670
+ if (!node) return;
6671
+ if (current === null) {
6672
+ clear(node);
6673
+ last.current = null;
6674
+ return;
6675
+ }
6676
+ place(node, current, last);
6677
+ const watching = observer.current;
6678
+ if (!watching) return;
6679
+ const next = [node, ...node.children];
6680
+ const before = watched.current;
6681
+ if (next.length === before.length && next.every((element, at) => element === before[at])) return;
6682
+ watching.disconnect();
6683
+ for (const element of next) watching.observe(element);
6684
+ watched.current = next;
6685
+ });
6686
+ useLayoutEffect(() => {
6687
+ const node = ref.current;
6688
+ if (!node || current === null || typeof ResizeObserver === "undefined") return;
6689
+ const watching = new ResizeObserver(() => place(node, current, last));
6690
+ const targets = [node, ...node.children];
6691
+ for (const element of targets) watching.observe(element);
6692
+ observer.current = watching;
6693
+ watched.current = targets;
6694
+ return () => {
6695
+ watching.disconnect();
6696
+ observer.current = null;
6697
+ watched.current = [];
6698
+ };
6699
+ }, [current]);
6700
+ return ref;
6701
+ }
6702
+ //#endregion
6134
6703
  //#region src/components/segmented-control/SegmentedControl.tsx
6135
6704
  const SegmentedContext = createContext(null);
6136
6705
  const track = {
@@ -6144,8 +6713,9 @@ const track = {
6144
6713
  *
6145
6714
  * 切换的是下面显示哪一块内容时用 `Tabs`,不是它。
6146
6715
  */
6147
- function SegmentedControl({ value, defaultValue, onValueChange, name, size = "md", variant = "sunken", disabled: disabledProp, required: requiredProp, invalid: invalidProp, className, ...props }) {
6716
+ function SegmentedControl({ value, defaultValue, onValueChange, name, size = "md", variant = "sunken", disabled: disabledProp, required: requiredProp, invalid: invalidProp, className, ref, ...props }) {
6148
6717
  const field = useFieldContext();
6718
+ const trackRef = useIndicator("[data-selected]");
6149
6719
  const autoName = useId();
6150
6720
  const [current, setCurrent] = useControllableState({
6151
6721
  value,
@@ -6185,9 +6755,10 @@ function SegmentedControl({ value, defaultValue, onValueChange, name, size = "md
6185
6755
  "aria-required": required || void 0,
6186
6756
  "aria-disabled": disabled || void 0,
6187
6757
  ...props,
6758
+ ref: mergeRefs(trackRef, ref),
6188
6759
  "data-variant": variant,
6189
6760
  "data-size": size,
6190
- className: cn("inline-grid max-w-full auto-cols-fr grid-flow-col p-0.5 text-ink", "transition-colors duration-(--duration-fast) ease-standard", track[variant], controlSize$1[size], disabled ? "border-line" : invalid ? "border-danger" : "border-line-strong focus-within:border-ink", className)
6761
+ className: cn("inline-grid max-w-full auto-cols-fr grid-flow-col p-0.5 text-ink", "transition-colors duration-(--duration-fast) ease-standard", indicator, indicatorBox, "after:bg-surface-inverse has-[[data-selected][data-disabled]]:after:bg-disabled", "forced-colors:after:bg-[Highlight] forced-colors:has-[[data-selected][data-disabled]]:after:bg-[GrayText]", track[variant], controlSize$1[size], disabled ? "border-line" : invalid ? "border-danger" : "border-line-strong focus-within:border-ink", className)
6191
6762
  })
6192
6763
  });
6193
6764
  }
@@ -6206,7 +6777,11 @@ function Segment({ value, icon, disabled: disabledProp, "aria-label": ariaLabel,
6206
6777
  ...props,
6207
6778
  "data-selected": checked ? "" : void 0,
6208
6779
  "data-disabled": disabled ? "" : void 0,
6209
- className: cn("relative flex min-w-0 items-center justify-center font-medium whitespace-nowrap select-none", "transition-colors duration-(--duration-fast) ease-standard", "has-focus-visible:z-1 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-focus", segmentPadding[context.size], disabled ? "cursor-not-allowed" : "cursor-pointer", checked ? disabled ? "bg-disabled text-on-disabled" : "bg-surface-inverse text-ink-inverse" : disabled ? "text-ink-disabled" : "text-ink-secondary hover:bg-ink/5 hover:text-ink", className),
6780
+ className: cn("relative flex min-w-0 items-center justify-center font-medium whitespace-nowrap select-none", "transition-colors duration-(--duration-fast) ease-standard", "has-focus-visible:z-1 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-focus", segmentPadding[context.size], disabled ? "cursor-not-allowed" : "cursor-pointer", checked ? [
6781
+ disabled ? "bg-disabled text-on-disabled forced-colors:bg-[GrayText] forced-colors:text-[Canvas]" : "bg-surface-inverse text-ink-inverse forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
6782
+ "in-data-indicator:bg-transparent forced-colors:in-data-indicator:bg-transparent",
6783
+ "forced-colors:forced-color-adjust-none forced-colors:has-focus-visible:outline-[Highlight]"
6784
+ ] : disabled ? "text-ink-disabled forced-colors:text-[GrayText]" : "text-ink-secondary hover:bg-ink/5 hover:text-ink", className),
6210
6785
  children: [
6211
6786
  /* @__PURE__ */ jsx("input", {
6212
6787
  type: "radio",
@@ -6705,7 +7280,7 @@ function SideRailSubItem({ current = false, end, disabled = false, href, target,
6705
7280
  }
6706
7281
  //#endregion
6707
7282
  //#region src/components/skeleton/Skeleton.tsx
6708
- const fill = "block bg-surface-muted";
7283
+ const fill = "block bg-surface-muted forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[GrayText]";
6709
7284
  /**
6710
7285
  * 骨架:只画大块,不画细节。
6711
7286
  * 自身对辅助技术隐藏;"正在加载"由外层区域的 `aria-busy="true"` 表达。
@@ -6744,8 +7319,11 @@ const thumb = [
6744
7319
  "after:absolute after:inset-y-1 after:left-1/2 after:w-0.5 after:-translate-x-1/2 after:bg-accent-ink-inverse after:opacity-0 after:transition-opacity after:duration-(--duration-fast) after:ease-standard after:content-[''] data-dragging:after:opacity-100",
6745
7320
  "choice-diamond:size-3.5 choice-diamond:rotate-45 choice-diamond:after:inset-1 choice-diamond:after:w-auto choice-diamond:after:translate-x-0",
6746
7321
  "data-disabled:bg-ink-disabled",
7322
+ "forced-colors:bg-[ButtonText] forced-colors:data-disabled:bg-[GrayText]",
6747
7323
  focusRingWithin
6748
7324
  ].join(" ");
7325
+ const trackForced = "forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[ButtonText] forced-colors:data-disabled:outline-[GrayText]";
7326
+ const indicatorForced = "forced-colors:bg-[Highlight] forced-colors:data-disabled:bg-[GrayText]";
6749
7327
  /**
6750
7328
  * 滑块:在一个范围里拖着选一个数。要的是确切的数时用 `Stepper`。
6751
7329
  * 每个滑块都要有名称:放进 `Field`,或者自己传 `aria-label`。
@@ -6795,8 +7373,8 @@ function Slider(props) {
6795
7373
  children: [/* @__PURE__ */ jsx(Slider$1.Control, {
6796
7374
  className: cn("flex touch-none items-center px-1.5 select-none data-disabled:cursor-not-allowed", controlSize[size]),
6797
7375
  children: /* @__PURE__ */ jsxs(Slider$1.Track, {
6798
- className: "h-1 w-full bg-line",
6799
- children: [/* @__PURE__ */ jsx(Slider$1.Indicator, { className: "bg-ink transition-[inset-inline-start,width] duration-(--duration-fast) ease-standard data-disabled:bg-ink-disabled data-dragging:transition-none" }), (range ? [0, 1] : [0]).map((index) => /* @__PURE__ */ jsx(Slider$1.Thumb, {
7376
+ className: cn("h-1 w-full bg-line", trackForced),
7377
+ children: [/* @__PURE__ */ jsx(Slider$1.Indicator, { className: cn("bg-ink transition-[inset-inline-start,width] duration-(--duration-fast) ease-standard data-disabled:bg-ink-disabled data-dragging:transition-none", indicatorForced) }), (range ? [0, 1] : [0]).map((index) => /* @__PURE__ */ jsx(Slider$1.Thumb, {
6800
7378
  index,
6801
7379
  ...range ? { "aria-label": thumbLabels[index] } : {
6802
7380
  "aria-label": ariaLabel,
@@ -6811,13 +7389,13 @@ function Slider(props) {
6811
7389
  className: "relative h-6 text-xs",
6812
7390
  children: marks.map((mark) => /* @__PURE__ */ jsx("span", {
6813
7391
  style: { left: markLeft((mark.value - min) / (max - min)) },
6814
- className: cn("absolute top-0 flex flex-col gap-0.5 whitespace-nowrap text-ink-secondary before:h-1.5 before:w-px before:bg-line-strong before:content-['']", mark.value <= min ? "items-start" : mark.value >= max ? "-translate-x-full items-end" : "-translate-x-1/2 items-center"),
7392
+ className: cn("absolute top-0 flex flex-col gap-0.5 whitespace-nowrap text-ink-secondary before:h-1.5 before:w-px before:bg-line-strong before:content-[''] forced-colors:before:bg-[ButtonText]", mark.value <= min ? "items-start" : mark.value >= max ? "-translate-x-full items-end" : "-translate-x-1/2 items-center"),
6815
7393
  children: mark.label
6816
7394
  }, mark.value))
6817
7395
  })]
6818
7396
  }), showValue && /* @__PURE__ */ jsx(Slider$1.Value, {
6819
7397
  style: { minWidth: valueWidth },
6820
- className: cn("flex shrink-0 items-center justify-end font-tech text-sm font-bold tabular-nums data-disabled:text-ink-disabled", controlSize[size]),
7398
+ className: cn("flex shrink-0 items-center justify-end font-tech text-sm font-bold tabular-nums data-disabled:text-ink-disabled forced-colors:data-disabled:text-[GrayText]", controlSize[size]),
6821
7399
  children: (formatted) => formatted.join(" – ")
6822
7400
  })]
6823
7401
  });
@@ -7007,7 +7585,7 @@ function Stepper({ value: valueProp, defaultValue, onValueChange, min, max, step
7007
7585
  onBlur?.(event);
7008
7586
  commitDraft();
7009
7587
  };
7010
- const buttonClass = (inactive) => cn("flex shrink-0 items-center justify-center select-none", "transition-colors duration-(--duration-fast) ease-standard", buttonSize[size], inactive ? "cursor-not-allowed bg-disabled text-on-disabled" : "bg-control text-on-control hover:bg-control-hover active:bg-control-pressed");
7588
+ const buttonClass = (inactive) => cn("flex shrink-0 items-center justify-center select-none", "transition-colors duration-(--duration-fast) ease-standard", "forced-colors:border forced-colors:border-[ButtonText] forced-colors:disabled:border-[GrayText]", buttonSize[size], inactive ? "cursor-not-allowed bg-disabled text-on-disabled" : "bg-control text-on-control hover:bg-control-hover active:bg-control-pressed");
7011
7589
  return /* @__PURE__ */ jsxs("div", {
7012
7590
  "data-size": size,
7013
7591
  "data-invalid": invalid ? "" : void 0,
@@ -7045,7 +7623,7 @@ function Stepper({ value: valueProp, defaultValue, onValueChange, min, max, step
7045
7623
  onChange: handleChange,
7046
7624
  onKeyDown: handleKeyDown,
7047
7625
  onBlur: handleBlur,
7048
- className: cn("min-w-0 flex-1 border-y-2 border-transparent bg-surface-sunken text-center font-tech font-bold tabular-nums outline-none", "disabled:cursor-not-allowed", fieldSize[size], disabled ? "text-ink-disabled" : "text-ink", invalid && !disabled && "border-b-danger")
7626
+ className: cn("min-w-0 flex-1 border-y-2 border-transparent bg-surface-sunken text-center font-tech font-bold tabular-nums outline-none", "disabled:cursor-not-allowed", "forced-colors:border-y forced-colors:border-[ButtonText] forced-colors:disabled:border-[GrayText]", fieldSize[size], disabled ? "text-ink-disabled" : "text-ink", invalid && !disabled && "border-b-danger")
7049
7627
  }),
7050
7628
  /* @__PURE__ */ jsx("button", {
7051
7629
  type: "button",
@@ -7084,8 +7662,8 @@ const railLayout = {
7084
7662
  horizontal: "flex-col @md:flex-row"
7085
7663
  };
7086
7664
  const lineLayout = {
7087
- vertical: "my-2 min-h-4 w-0.5",
7088
- horizontal: "my-2 min-h-4 w-0.5 @md:mx-2 @md:my-0 @md:h-0.5 @md:min-h-0 @md:w-auto"
7665
+ vertical: "my-2 min-h-4 w-0.5 forced-colors:border-r",
7666
+ horizontal: "my-2 min-h-4 w-0.5 @md:mx-2 @md:my-0 @md:h-0.5 @md:min-h-0 @md:w-auto forced-colors:@max-md:border-r forced-colors:@md:border-b"
7089
7667
  };
7090
7668
  const bodyLayout = {
7091
7669
  vertical: "pb-4 group-last/step:pb-0",
@@ -7128,8 +7706,8 @@ const nodeSize = {
7128
7706
  md: "size-6 text-xs [&_svg]:size-3.5"
7129
7707
  };
7130
7708
  const nodeClass$1 = {
7131
- done: "bg-surface-inverse text-accent-ink-inverse",
7132
- current: "border border-ink bg-action font-bold text-on-action",
7709
+ done: "bg-surface-inverse text-accent-ink-inverse forced-colors:bg-[CanvasText] forced-colors:text-[Canvas]",
7710
+ current: "border border-ink bg-action font-bold text-on-action forced-colors:forced-color-adjust-none forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
7133
7711
  upcoming: "border border-line-strong text-ink-secondary"
7134
7712
  };
7135
7713
  const titleSize = {
@@ -7165,7 +7743,7 @@ function Step({ title, description, invalid = false, href, target, rel, render,
7165
7743
  children: [/* @__PURE__ */ jsx("span", {
7166
7744
  className: cn("flex shrink-0 items-center justify-center font-tech leading-none tabular-nums", "transition-colors duration-(--duration-fast) ease-standard", nodeSize[size], invalid ? "border border-danger text-danger" : nodeClass$1[status]),
7167
7745
  children: invalid ? /* @__PURE__ */ jsx(StatusDanger, {}) : status === "done" ? /* @__PURE__ */ jsx(Check, {}) : String(index + 1).padStart(2, "0")
7168
- }), /* @__PURE__ */ jsx("span", { className: cn("relative flex-1 overflow-clip bg-line-strong group-last/step:hidden", "before:absolute before:top-0 before:-right-4 before:-bottom-4 before:left-0 before:origin-top-left before:bg-ink before:transition-[scale] before:duration-(--duration-base) before:ease-exit before:content-['']", lineLayout[orientation], status === "done" ? "before:scale-100" : "before:scale-0") })]
7746
+ }), /* @__PURE__ */ jsx("span", { className: cn("relative flex-1 overflow-clip bg-line-strong group-last/step:hidden", "forced-colors:border-[CanvasText] forced-colors:before:bg-[CanvasText]", "before:absolute before:top-0 before:-right-4 before:-bottom-4 before:left-0 before:origin-top-left before:bg-ink before:transition-[scale] before:duration-(--duration-base) before:ease-exit before:content-['']", lineLayout[orientation], status === "done" ? "before:scale-100" : "before:scale-0") })]
7169
7747
  }), /* @__PURE__ */ jsxs("div", {
7170
7748
  className: cn("min-w-0 flex-1", bodyLayout[orientation]),
7171
7749
  children: [/* @__PURE__ */ jsx("div", {
@@ -7195,6 +7773,8 @@ function Step({ title, description, invalid = false, href, target, rel, render,
7195
7773
  //#region src/components/switch/Switch.tsx
7196
7774
  const trackTransition = "transition-colors duration-(--duration-fast) ease-standard";
7197
7775
  const dualLabel = "pointer-events-none relative min-w-9 px-3 text-center transition-colors duration-(--duration-fast) ease-standard peer-disabled:text-ink-disabled";
7776
+ const dualOffForced = "forced-colors:forced-color-adjust-none forced-colors:text-[HighlightText] forced-colors:peer-checked:text-[CanvasText] forced-colors:peer-disabled:text-[Canvas] forced-colors:peer-checked:peer-disabled:text-[GrayText]";
7777
+ const dualOnForced = "forced-colors:forced-color-adjust-none forced-colors:text-[CanvasText] forced-colors:peer-checked:text-[HighlightText] forced-colors:peer-disabled:text-[GrayText] forced-colors:peer-checked:peer-disabled:text-[Canvas]";
7198
7778
  /**
7199
7779
  * 开关:胶囊轨道 + 圆形旋钮,立即生效的设置用它(需要"保存"才生效的用复选框)。
7200
7780
  * 原生 `<input type="checkbox" role="switch">`,受控用 `checked`、非受控用 `defaultChecked`。
@@ -7216,22 +7796,22 @@ function Switch({ onCheckedChange, onChange, offLabel, onLabel, className, child
7216
7796
  type: "checkbox",
7217
7797
  role: "switch",
7218
7798
  onChange: handleChange,
7219
- className: cn("peer absolute inset-0 size-full appearance-none rounded-full bg-surface-inverse/80 group-hover:bg-surface-inverse disabled:cursor-not-allowed disabled:bg-surface-muted", trackTransition, focusRing)
7799
+ className: cn("peer absolute inset-0 size-full appearance-none rounded-full bg-surface-inverse/80 group-hover:bg-surface-inverse disabled:cursor-not-allowed disabled:bg-surface-muted", "forced-colors:border forced-colors:border-[ButtonText] forced-colors:disabled:border-[GrayText]", trackTransition, focusRing)
7220
7800
  }),
7221
7801
  /* @__PURE__ */ jsx("span", {
7222
7802
  "aria-hidden": "true",
7223
- className: cn("pointer-events-none absolute inset-y-1 left-1 w-[calc(50%-0.25rem)] rounded-full bg-surface-muted", "transition-[translate] duration-(--duration-fast) ease-standard", "peer-checked:translate-x-full peer-disabled:bg-surface-raised")
7803
+ className: cn("pointer-events-none absolute inset-y-1 left-1 w-[calc(50%-0.25rem)] rounded-full bg-surface-muted", "transition-[translate] duration-(--duration-fast) ease-standard", "peer-checked:translate-x-full peer-disabled:bg-surface-raised", "forced-colors:bg-[Highlight] forced-colors:peer-disabled:bg-[GrayText]")
7224
7804
  }),
7225
7805
  /* @__PURE__ */ jsx("span", {
7226
7806
  "aria-hidden": "true",
7227
7807
  "data-value": "off",
7228
- className: cn(dualLabel, "text-ink peer-checked:text-ink-inverse/70"),
7808
+ className: cn(dualLabel, "text-ink peer-checked:text-ink-inverse/70", dualOffForced),
7229
7809
  children: offLabel
7230
7810
  }),
7231
7811
  /* @__PURE__ */ jsx("span", {
7232
7812
  "aria-hidden": "true",
7233
7813
  "data-value": "on",
7234
- className: cn(dualLabel, "text-ink-inverse/70 peer-checked:text-ink"),
7814
+ className: cn(dualLabel, "text-ink-inverse/70 peer-checked:text-ink", dualOnForced),
7235
7815
  children: onLabel
7236
7816
  })
7237
7817
  ]
@@ -7242,10 +7822,10 @@ function Switch({ onCheckedChange, onChange, offLabel, onLabel, className, child
7242
7822
  type: "checkbox",
7243
7823
  role: "switch",
7244
7824
  onChange: handleChange,
7245
- className: cn("peer h-7 w-13 appearance-none rounded-full bg-line-strong group-hover:bg-ink-tertiary checked:bg-surface-inverse disabled:cursor-not-allowed disabled:bg-surface-muted", trackTransition, focusRing)
7825
+ className: cn("peer h-7 w-13 appearance-none rounded-full bg-line-strong group-hover:bg-ink-tertiary checked:bg-surface-inverse disabled:cursor-not-allowed disabled:bg-surface-muted", "forced-colors:border-2 forced-colors:border-[ButtonText] forced-colors:checked:border-[Highlight] forced-colors:checked:bg-[Highlight]", "forced-colors:disabled:border-[GrayText] forced-colors:checked:disabled:bg-[GrayText]", trackTransition, focusRing)
7246
7826
  }), /* @__PURE__ */ jsx("span", {
7247
7827
  "aria-hidden": "true",
7248
- className: cn("pointer-events-none absolute top-1 left-1 size-5 rounded-full bg-surface-raised", "transition-[translate,background-color] duration-(--duration-fast) ease-standard", "peer-checked:translate-x-6 peer-checked:bg-accent-ink-inverse peer-disabled:bg-ink-disabled")
7828
+ className: cn("pointer-events-none absolute top-1 left-1 size-5 rounded-full bg-surface-raised", "transition-[translate,background-color] duration-(--duration-fast) ease-standard", "peer-checked:translate-x-6 peer-checked:bg-accent-ink-inverse peer-disabled:bg-ink-disabled", "forced-colors:bg-[ButtonText] forced-colors:peer-checked:bg-[HighlightText] forced-colors:peer-disabled:bg-[GrayText] forced-colors:peer-checked:peer-disabled:bg-[Canvas]")
7249
7829
  })]
7250
7830
  });
7251
7831
  return /* @__PURE__ */ jsxs("label", {
@@ -7336,7 +7916,7 @@ function TableHead(props) {
7336
7916
  })
7337
7917
  });
7338
7918
  }
7339
- const ruledLines = ["[&>tr:nth-child(5n_of_:not([data-detail])):not([data-expanded])>*]:border-line-strong", "[&>tr:nth-child(5n_of_:not([data-detail]))+tr[data-detail]>*]:border-line-strong"].join(" ");
7919
+ const ruledLines = ["[&>tr:nth-child(5n_of_:not([data-detail])):not(:has(+tr[data-detail]))>*]:border-line-strong", "[&>tr:nth-child(5n_of_:not([data-detail]))+tr[data-detail]>*]:border-line-strong"].join(" ");
7340
7920
  function TableBody({ className, ...props }) {
7341
7921
  const { ruled } = useContext(TableContext);
7342
7922
  return /* @__PURE__ */ jsx(SectionContext, {
@@ -7360,6 +7940,7 @@ function TableRow({ selected, detail, expanded: expandedProp, defaultExpanded =
7360
7940
  });
7361
7941
  const expandable = detail !== void 0 && detail !== null && detail !== false;
7362
7942
  const open = expandable && expanded;
7943
+ const detailPresence = usePresence(open);
7363
7944
  const context = useMemo(() => expandable ? {
7364
7945
  expanded: open,
7365
7946
  toggle: () => setExpanded(!open),
@@ -7382,15 +7963,16 @@ function TableRow({ selected, detail, expanded: expandedProp, defaultExpanded =
7382
7963
  "aria-selected": selected,
7383
7964
  "data-selected": selected ? "" : void 0,
7384
7965
  "data-expanded": open ? "" : void 0,
7385
- className: cn("group/row transition-colors duration-(--duration-fast) ease-standard", selected ? rowSelected : rowIdle, "[&[data-expanded]>*]:border-b-transparent", className),
7966
+ className: cn("group/row transition-colors duration-(--duration-fast) ease-standard", selected ? rowSelected : rowIdle, "[&:has(+tr[data-detail])>*]:border-b-transparent", className),
7386
7967
  children: /* @__PURE__ */ jsx(RowContext, {
7387
7968
  value: context,
7388
7969
  children
7389
7970
  })
7390
- }), open && /* @__PURE__ */ jsx(DetailRow, {
7971
+ }), detailPresence.mounted && /* @__PURE__ */ jsx(DetailRow, {
7391
7972
  id: detailId,
7392
7973
  rowRef,
7393
7974
  estimate: countCells(children),
7975
+ presence: detailPresence,
7394
7976
  children: detail
7395
7977
  })] });
7396
7978
  }
@@ -7403,7 +7985,7 @@ function countCells(children) {
7403
7985
  }
7404
7986
  return Math.max(total, 1);
7405
7987
  }
7406
- function DetailRow({ id, rowRef, estimate, children }) {
7988
+ function DetailRow({ id, rowRef, estimate, presence, children }) {
7407
7989
  const [measured, setMeasured] = useState(null);
7408
7990
  useLayoutEffect(() => {
7409
7991
  const row = rowRef.current;
@@ -7415,12 +7997,21 @@ function DetailRow({ id, rowRef, estimate, children }) {
7415
7997
  return /* @__PURE__ */ jsx("tr", {
7416
7998
  id,
7417
7999
  "data-detail": "",
8000
+ inert: presence.leaving,
7418
8001
  children: /* @__PURE__ */ jsx("td", {
7419
8002
  colSpan: measured ?? estimate,
7420
8003
  className: "border-b border-line bg-surface-sunken p-0 [tr:last-child>&]:border-b-0",
7421
8004
  children: /* @__PURE__ */ jsx("div", {
7422
- className: "sticky left-0 w-[var(--table-viewport,100%)] px-4 py-3 text-sm whitespace-normal",
7423
- children
8005
+ ref: presence.ref,
8006
+ "data-leaving": presence.leaving ? "" : void 0,
8007
+ className: cn("sticky left-0 w-[var(--table-viewport,100%)]", collapse, presence.entered && "starting:grid-rows-[0fr]"),
8008
+ children: /* @__PURE__ */ jsx("div", {
8009
+ className: collapseInner,
8010
+ children: /* @__PURE__ */ jsx("div", {
8011
+ className: "px-4 py-3 text-sm whitespace-normal",
8012
+ children
8013
+ })
8014
+ })
7424
8015
  })
7425
8016
  })
7426
8017
  });
@@ -7521,18 +8112,23 @@ function Tabs({ value, defaultValue, onValueChange, variant = "block", size = "m
7521
8112
  defaultValue: defaultValue ?? "",
7522
8113
  onChange: onValueChange
7523
8114
  });
8115
+ const [initial] = useState(current);
8116
+ const [switched, setSwitched] = useState(false);
8117
+ if (!switched && current !== initial) setSwitched(true);
7524
8118
  const context = useMemo(() => ({
7525
8119
  value: current,
7526
8120
  select,
7527
8121
  baseId,
7528
8122
  variant,
7529
- size
8123
+ size,
8124
+ switched
7530
8125
  }), [
7531
8126
  current,
7532
8127
  select,
7533
8128
  baseId,
7534
8129
  variant,
7535
- size
8130
+ size,
8131
+ switched
7536
8132
  ]);
7537
8133
  return /* @__PURE__ */ jsx(TabsContext, {
7538
8134
  value: context,
@@ -7542,8 +8138,9 @@ function Tabs({ value, defaultValue, onValueChange, variant = "block", size = "m
7542
8138
  })
7543
8139
  });
7544
8140
  }
7545
- function TabList({ className, onKeyDown, ...props }) {
8141
+ function TabList({ className, onKeyDown, ref, ...props }) {
7546
8142
  const { variant } = useTabsContext("TabList");
8143
+ const listRef = useIndicator(variant === "capsule" ? "[role=\"tab\"][aria-selected=\"true\"]" : null);
7547
8144
  const handleKeyDown = (event) => {
7548
8145
  onKeyDown?.(event);
7549
8146
  if (event.defaultPrevented) return;
@@ -7573,10 +8170,17 @@ function TabList({ className, onKeyDown, ...props }) {
7573
8170
  };
7574
8171
  return /* @__PURE__ */ jsx("div", {
7575
8172
  ...props,
8173
+ ref: mergeRefs(listRef, ref),
7576
8174
  role: "tablist",
7577
8175
  "aria-orientation": "horizontal",
7578
8176
  onKeyDown: handleKeyDown,
7579
- className: cn("flex overflow-x-auto [scrollbar-width:thin]", variant === "capsule" && "gap-2 p-1", variant === "wedge" && "border-b-2 border-ink", className)
8177
+ className: cn("flex overflow-x-auto [scrollbar-width:thin]", variant === "capsule" && [
8178
+ "gap-2 p-1",
8179
+ indicator,
8180
+ indicatorBox,
8181
+ "after:z-0 after:rounded-full after:bg-surface-inverse",
8182
+ "forced-colors:after:bg-[Highlight]"
8183
+ ], variant === "wedge" && "border-b-2 border-ink", className)
7580
8184
  });
7581
8185
  }
7582
8186
  const blockSize = {
@@ -7616,12 +8220,15 @@ function Tab({ value, disabled = false, className, children, onClick, ...props }
7616
8220
  };
7617
8221
  if (variant === "capsule") return /* @__PURE__ */ jsx("button", {
7618
8222
  ...shared,
7619
- className: cn(capsuleBase, focusRing, capsuleSize[size], selected ? capsuleState.selected : disabled ? capsuleState.disabled : capsuleState.rest, className),
7620
- children
8223
+ className: cn(capsuleBase, focusRing, capsuleSize[size], selected ? [capsuleState.selected, "in-data-indicator:bg-transparent forced-colors:in-data-indicator:bg-transparent"] : disabled ? capsuleState.disabled : capsuleState.rest, className),
8224
+ children: /* @__PURE__ */ jsx("span", {
8225
+ className: "relative z-1 inline-flex items-center gap-[inherit]",
8226
+ children
8227
+ })
7621
8228
  });
7622
8229
  if (variant === "wedge") return /* @__PURE__ */ jsx("button", {
7623
8230
  ...shared,
7624
- className: cn("relative isolate inline-flex shrink-0 items-center justify-center leading-none font-medium whitespace-nowrap", "transition-colors duration-(--duration-fast) ease-standard", "before:absolute before:inset-0 before:-z-10 before:wedge-r before:transition-colors before:duration-(--duration-fast) before:ease-standard before:content-['']", focusRingInset, wedgeSize[size], selected ? "text-on-action before:bg-action" : disabled ? "cursor-not-allowed text-ink-disabled" : "text-ink hover:before:bg-ink/5", className),
8231
+ className: cn("relative isolate inline-flex shrink-0 items-center justify-center leading-none font-medium whitespace-nowrap", "transition-colors duration-(--duration-fast) ease-standard", "before:absolute before:inset-0 before:-z-10 before:wedge-r before:transition-colors before:duration-(--duration-fast) before:ease-standard before:content-['']", focusRingInset, wedgeSize[size], selected ? "text-on-action before:bg-action focus-visible:-outline-offset-4 focus-visible:outline-on-action forced-colors:forced-color-adjust-none forced-colors:text-[HighlightText] forced-colors:before:bg-[Highlight] forced-colors:focus-visible:-outline-offset-4 forced-colors:focus-visible:outline-[HighlightText]" : disabled ? "cursor-not-allowed text-ink-disabled" : "text-ink hover:before:bg-ink/5", className),
7625
8232
  children
7626
8233
  });
7627
8234
  return /* @__PURE__ */ jsxs("button", {
@@ -7637,7 +8244,10 @@ function Tab({ value, disabled = false, className, children, onClick, ...props }
7637
8244
  })]
7638
8245
  });
7639
8246
  }
7640
- /** 未选中的面板保留一个隐藏的空容器,让页签的 `aria-controls` 始终有所指。 */
8247
+ /**
8248
+ * 未选中的面板保留一个隐藏的空容器,让页签的 `aria-controls` 始终有所指。
8249
+ * 切过来的面板淡入;一开始就选中的那一块不动。
8250
+ */
7641
8251
  function TabPanel({ value, className, children, ...props }) {
7642
8252
  const context = useTabsContext("TabPanel");
7643
8253
  const selected = context.value === value;
@@ -7648,7 +8258,7 @@ function TabPanel({ value, className, children, ...props }) {
7648
8258
  "aria-labelledby": toId(context.baseId, "tab", value),
7649
8259
  hidden: !selected,
7650
8260
  tabIndex: 0,
7651
- className: cn(focusRing, className),
8261
+ className: cn(focusRing, selected && context.switched && "animate-fade-in [animation-duration:var(--duration-fast)]", className),
7652
8262
  children: selected ? children : null
7653
8263
  });
7654
8264
  }
@@ -7671,6 +8281,170 @@ function TagPair({ name, value, emphasis = false, size = "md", className, ...pro
7671
8281
  });
7672
8282
  }
7673
8283
  //#endregion
8284
+ //#region src/lib/inline-style.ts
8285
+ /**
8286
+ * 往元素上临时写过内联样式、又都撤掉之后调:原来没有 `style` 属性的,
8287
+ * 不留一个空的在上面。
8288
+ *
8289
+ * 先问一句"有没有这个属性"不是多余的:浏览器是懒着把内联样式同步成属性的,
8290
+ * 不问就去删,删的那一下落空,回头它又冒出来一个空的 `style=""`。
8291
+ */
8292
+ function dropEmptyStyle(node) {
8293
+ if (node.style.length === 0 && node.hasAttribute("style")) node.removeAttribute("style");
8294
+ }
8295
+ const px = (value) => `${Math.round(value * 100) / 100}px`;
8296
+ /** 这个元素眼下挂着的位移:补位滑到一半时不是 0 */
8297
+ function flight(node) {
8298
+ const value = getComputedStyle(node).translate;
8299
+ if (!value || value === "none") return [0, 0];
8300
+ const [x = 0, y = 0] = value.split(" ").map((part) => Number.parseFloat(part) || 0);
8301
+ return [x, y];
8302
+ }
8303
+ /** 把写上去的内联样式撤干净:原来没有 style 属性的,不留一个空的在上面 */
8304
+ function unset(node, names) {
8305
+ for (const name of names) node.style.removeProperty(name);
8306
+ dropEmptyStyle(node);
8307
+ }
8308
+ const PIN = [
8309
+ "left",
8310
+ "top",
8311
+ "width",
8312
+ "height"
8313
+ ];
8314
+ /** 往走的那一项上写过的全部:钉它的那四样,钉不住时的藏 */
8315
+ const HELD = [...PIN, "display"];
8316
+ /**
8317
+ * 量一遍、记下来。这一次有一项刚走,就补位:走的钉在原处,
8318
+ * 其余挪了地方的先摆回旧位置(不过渡),再撤掉——撤的那一下滑回原位;
8319
+ * 容器因此变矮了的话,高度也从原来的收到新的。
8320
+ */
8321
+ function arrange(node, memory) {
8322
+ const { seen, pinned } = memory;
8323
+ const items = [...node.children];
8324
+ const leaving = (item) => item.hasAttribute("data-leaving");
8325
+ for (const item of items) {
8326
+ if (leaving(item) || !pinned.has(item)) continue;
8327
+ pinned.delete(item);
8328
+ unset(item, HELD);
8329
+ }
8330
+ const box = node.getBoundingClientRect();
8331
+ if (box.width === 0 || box.height === 0 || items.filter((item) => !leaving(item)).length > 50) {
8332
+ seen.clear();
8333
+ memory.height = 0;
8334
+ for (const item of items) {
8335
+ if (!leaving(item) || pinned.has(item)) continue;
8336
+ pinned.add(item);
8337
+ item.style.setProperty("display", "none");
8338
+ }
8339
+ return;
8340
+ }
8341
+ const laid = Number.parseFloat(getComputedStyle(node).width);
8342
+ const scale = laid > 0 ? box.width / laid : 1;
8343
+ const gone = [];
8344
+ const moved = [];
8345
+ const next = [];
8346
+ for (const item of items) {
8347
+ const before = seen.get(item);
8348
+ if (leaving(item)) {
8349
+ if (!pinned.has(item)) gone.push([item, before]);
8350
+ continue;
8351
+ }
8352
+ const rect = item.getBoundingClientRect();
8353
+ const [x, y] = flight(item);
8354
+ const place = {
8355
+ left: (rect.left - box.left) / scale - node.clientLeft + node.scrollLeft - x,
8356
+ top: (rect.top - box.top) / scale - node.clientTop + node.scrollTop - y,
8357
+ width: rect.width / scale,
8358
+ height: rect.height / scale
8359
+ };
8360
+ next.push([item, place]);
8361
+ if (!before) continue;
8362
+ const dx = before.left + x - place.left;
8363
+ const dy = before.top + y - place.top;
8364
+ if (Math.abs(dx) >= .5 || Math.abs(dy) >= .5) moved.push([
8365
+ item,
8366
+ dx,
8367
+ dy
8368
+ ]);
8369
+ }
8370
+ seen.clear();
8371
+ for (const [item, place] of next) seen.set(item, place);
8372
+ const tall = box.height / scale;
8373
+ const shrinking = memory.settle !== null;
8374
+ const from = shrinking ? tall : memory.height;
8375
+ if (!shrinking) memory.height = tall;
8376
+ if (gone.length === 0) return;
8377
+ for (const [item, place] of gone) {
8378
+ pinned.add(item);
8379
+ if (place) PIN.forEach((name) => item.style.setProperty(name, px(place[name])));
8380
+ else item.style.setProperty("display", "none");
8381
+ }
8382
+ memory.settle?.();
8383
+ memory.settle = null;
8384
+ node.style.setProperty("transition", "none");
8385
+ node.style.removeProperty("min-height");
8386
+ const to = node.getBoundingClientRect().height / scale;
8387
+ memory.height = to;
8388
+ const shrink = from - to >= .5;
8389
+ if (shrink) node.style.setProperty("min-height", px(from));
8390
+ for (const [item, dx, dy] of moved) {
8391
+ item.style.setProperty("transition", "none");
8392
+ item.style.setProperty("translate", `${px(dx)} ${px(dy)}`);
8393
+ }
8394
+ node.getBoundingClientRect();
8395
+ for (const [item] of moved) unset(item, ["transition", "translate"]);
8396
+ node.style.removeProperty("transition");
8397
+ if (shrink) {
8398
+ node.style.setProperty("min-height", px(to));
8399
+ memory.settle = afterMotion(node, () => {
8400
+ memory.settle = null;
8401
+ unset(node, ["min-height"]);
8402
+ });
8403
+ } else dropEmptyStyle(node);
8404
+ }
8405
+ /**
8406
+ * 补位:容器里有一项走了,其余的直接子元素从旧位置滑到新位置,不是瞬移过去。
8407
+ * 返回的 `ref` 给容器。
8408
+ *
8409
+ * - 走的那一项要带 `data-leaving`、并且脱离排版(`lib/motion.ts` 的 `flipLeaving`):
8410
+ * 钩子把它钉在原来的位置上,它在那里把退场走完;容器要是定位的参照(`flipBox`)。
8411
+ * - 其余各项要给 `translate` 加上过渡:钩子只负责把它们摆回旧位置再放手,
8412
+ * 滑回来靠的是 CSS——所以系统开了"减少动态效果"时是直接到位的。
8413
+ * - 容器因此变矮了(少了一行),它的高度也从原来的收到新的(量着收,过渡同样在
8414
+ * `flipBox` 里);收完撤掉,不留在容器上。
8415
+ * - 各项只动 `translate`。正滑到一半又少一项,从眼前的位置接着走。
8416
+ * - 只有"少了一项"才补位;加一项、容器尺寸变了,各项直接到位。
8417
+ * - 超过 `FLIP_LIMIT` 项、量不到位置时不做:走的那一项也不留,照旧直接消失。
8418
+ */
8419
+ function useFlip() {
8420
+ const ref = useRef(null);
8421
+ const memory = useRef({
8422
+ seen: /* @__PURE__ */ new Map(),
8423
+ height: 0,
8424
+ pinned: /* @__PURE__ */ new WeakSet(),
8425
+ settle: null
8426
+ });
8427
+ useLayoutEffect(() => {
8428
+ const node = ref.current;
8429
+ if (node) arrange(node, memory.current);
8430
+ });
8431
+ useLayoutEffect(() => {
8432
+ const node = ref.current;
8433
+ if (!node || typeof ResizeObserver === "undefined") return;
8434
+ const observer = new ResizeObserver(() => arrange(node, memory.current));
8435
+ observer.observe(node);
8436
+ return () => observer.disconnect();
8437
+ }, []);
8438
+ useEffect(() => {
8439
+ const held = memory.current;
8440
+ return () => {
8441
+ held.settle?.();
8442
+ held.settle = null;
8443
+ };
8444
+ }, []);
8445
+ return ref;
8446
+ }
8447
+ //#endregion
7674
8448
  //#region src/components/tag-input/TagInput.tsx
7675
8449
  const defaultLabels = {
7676
8450
  summary: (tags) => tags.length === 0 ? "还没有标签" : `已有 ${tags.length} 个标签:${tags.join("、")}`,
@@ -7718,6 +8492,9 @@ function TagInput({ value, defaultValue, onValueChange, name, max, validate, sep
7718
8492
  const [flash, setFlash] = useState(null);
7719
8493
  const [message, setMessage] = useState("");
7720
8494
  const full = max !== void 0 && tags.length >= max;
8495
+ const added = useAddedKeys(tags);
8496
+ const rowRef = useFlip();
8497
+ const lingering = useLingering(tags, tags.length < 50);
7721
8498
  useEffect(() => {
7722
8499
  if (flash === null) return;
7723
8500
  const timer = setTimeout(() => setFlash(null), FLASH_MS);
@@ -7875,24 +8652,31 @@ function TagInput({ value, defaultValue, onValueChange, name, max, validate, sep
7875
8652
  }), "items-center", multiValueBox[size], className),
7876
8653
  children: [
7877
8654
  /* @__PURE__ */ jsxs("div", {
7878
- className: "flex min-w-0 flex-1 flex-wrap items-center gap-1 py-1 pl-2",
7879
- children: [tags.map((tag, index) => /* @__PURE__ */ jsxs("span", {
7880
- "data-tag": "",
7881
- "data-flash": flash === tag ? "" : void 0,
7882
- className: cn(valueChip, valueChipSize[size], "transition-colors duration-(--duration-fast) ease-standard", "data-flash:bg-surface-inverse data-flash:text-ink-inverse", disabled && "text-ink-disabled", !editable && "pr-2"),
7883
- children: [/* @__PURE__ */ jsx("span", {
7884
- className: "min-w-0 truncate",
7885
- children: tag
7886
- }), editable && /* @__PURE__ */ jsx("button", {
7887
- type: "button",
7888
- tabIndex: -1,
7889
- "aria-label": removeLabel(tag),
7890
- onClick: (event) => remove(index, event.detail === 0 && tags.length > 1 ? index : "input"),
7891
- onKeyDown: (event) => handleChipKeyDown(event, index),
7892
- className: cn("flex aspect-square h-full shrink-0 items-center justify-center text-ink-secondary hover:bg-line hover:text-ink", "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus", flash === tag && "text-ink-inverse"),
7893
- children: /* @__PURE__ */ jsx(Close, { size: 12 })
7894
- })]
7895
- }, tag)), /* @__PURE__ */ jsx("input", {
8655
+ ref: mergeRefs(rowRef, lingering.ref),
8656
+ className: cn("flex min-w-0 flex-1 flex-wrap items-center gap-1 py-1 pl-2", flipBox),
8657
+ children: [lingering.entries.map(({ key: tag, leaving }) => {
8658
+ const index = tags.indexOf(tag);
8659
+ return /* @__PURE__ */ jsxs("span", {
8660
+ "data-tag": leaving ? void 0 : "",
8661
+ "data-leaving": leaving ? "" : void 0,
8662
+ "data-flash": flash === tag && !leaving ? "" : void 0,
8663
+ inert: leaving,
8664
+ "aria-hidden": leaving || void 0,
8665
+ className: cn(valueChip, valueChipSize[size], "transition-[color,background-color,translate,opacity] duration-(--duration-fast) ease-standard", "data-flash:bg-surface-inverse data-flash:text-ink-inverse", "forced-colors:data-flash:outline-2 forced-colors:data-flash:outline-[Highlight]", flipLeaving, added.has(tag) && "animate-fade-in [animation-duration:var(--duration-fast)]", disabled && "text-ink-disabled forced-colors:border-[GrayText] forced-colors:text-[GrayText]", !editable && "pr-2"),
8666
+ children: [/* @__PURE__ */ jsx("span", {
8667
+ className: "min-w-0 truncate",
8668
+ children: tag
8669
+ }), editable && /* @__PURE__ */ jsx("button", {
8670
+ type: "button",
8671
+ tabIndex: -1,
8672
+ "aria-label": removeLabel(tag),
8673
+ onClick: (event) => remove(index, event.detail === 0 && tags.length > 1 ? index : "input"),
8674
+ onKeyDown: (event) => handleChipKeyDown(event, index),
8675
+ className: cn("flex aspect-square h-full shrink-0 items-center justify-center text-ink-secondary hover:bg-line hover:text-ink forced-colors:in-data-disabled:text-[GrayText]", "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus", flash === tag && "text-ink-inverse"),
8676
+ children: /* @__PURE__ */ jsx(Close, { size: 12 })
8677
+ })]
8678
+ }, tag);
8679
+ }), /* @__PURE__ */ jsx("input", {
7896
8680
  ...props,
7897
8681
  ref: mergeRefs(inputRef, ref),
7898
8682
  id,
@@ -7917,7 +8701,7 @@ function TagInput({ value, defaultValue, onValueChange, name, max, validate, sep
7917
8701
  onBlur?.(event);
7918
8702
  if (commitOnBlur && editable && text.trim() !== "") setText(commit([text]));
7919
8703
  },
7920
- className: cn(controlElement, "min-w-12 px-1", valueInputHeight[size])
8704
+ className: cn(controlElement, "min-w-12 px-1", valueInputHeight[size], "transition-[translate] duration-(--duration-fast) ease-standard")
7921
8705
  })]
7922
8706
  }),
7923
8707
  (invalid || max !== void 0) && /* @__PURE__ */ jsxs("span", {
@@ -7928,7 +8712,7 @@ function TagInput({ value, defaultValue, onValueChange, name, max, validate, sep
7928
8712
  }), max !== void 0 && /* @__PURE__ */ jsx("span", {
7929
8713
  "aria-hidden": "true",
7930
8714
  "data-count": "",
7931
- className: cn("font-tech text-xs tabular-nums", disabled ? "text-ink-disabled" : full ? "text-ink" : "text-ink-secondary"),
8715
+ className: cn("font-tech text-xs tabular-nums", disabled ? "text-ink-disabled forced-colors:text-[GrayText]" : full ? "text-ink" : "text-ink-secondary"),
7932
8716
  children: `${tags.length} / ${max}`
7933
8717
  })]
7934
8718
  }),
@@ -8020,7 +8804,7 @@ function Textarea({ variant = "sunken", invalid: invalidProp, showCount = false,
8020
8804
  className: cn(controlElement, "block min-h-[calc(3lh+1rem)] w-full resize-y px-3 py-2")
8021
8805
  }), showCount && /* @__PURE__ */ jsx("span", {
8022
8806
  "data-count": "",
8023
- className: cn("pointer-events-none self-end px-3 pb-1.5 font-tech text-xs tabular-nums", disabled ? "text-ink-disabled" : "text-ink-secondary"),
8807
+ className: cn("pointer-events-none self-end px-3 pb-1.5 font-tech text-xs tabular-nums", disabled ? "text-ink-disabled forced-colors:text-[GrayText]" : "text-ink-secondary"),
8024
8808
  children: maxLength !== void 0 ? `${length} / ${maxLength}` : length
8025
8809
  })]
8026
8810
  });
@@ -8089,8 +8873,8 @@ function Timeline({ className, ...props }) {
8089
8873
  });
8090
8874
  }
8091
8875
  const nodeClass = {
8092
- past: "bg-ink",
8093
- current: "border border-ink bg-action",
8876
+ past: "bg-ink forced-colors:bg-[CanvasText]",
8877
+ current: "border border-ink bg-action forced-colors:bg-[Highlight]",
8094
8878
  upcoming: "border-2 border-ink-tertiary"
8095
8879
  };
8096
8880
  const titleClass = {
@@ -8111,7 +8895,7 @@ function TimelineItem({ status = "past", date, title, className, children, ...pr
8111
8895
  children: [/* @__PURE__ */ jsx("span", {
8112
8896
  className: "flex h-[1lh] min-h-4 w-4 items-center justify-center",
8113
8897
  children: /* @__PURE__ */ jsx("span", { className: cn("size-[11px] rotate-45", nodeClass[status]) })
8114
- }), /* @__PURE__ */ jsx("span", { className: "w-0.5 flex-1 bg-line-strong group-last/item:hidden" })]
8898
+ }), /* @__PURE__ */ jsx("span", { className: "w-0.5 flex-1 bg-line-strong group-last/item:hidden forced-colors:bg-[CanvasText]" })]
8115
8899
  }), /* @__PURE__ */ jsxs("div", {
8116
8900
  className: "min-w-0 flex-1 pb-6 group-last/item:pb-0",
8117
8901
  children: [
@@ -8257,13 +9041,42 @@ const TocContext = createContext({
8257
9041
  /** 点了一项之后,多久没有滚动事件算这一次滚动停下了 */
8258
9042
  const SETTLE_MS = 150;
8259
9043
  /**
9044
+ * 让接下来的这一次滚动是平滑的:给这几个滚动容器写上内联的 `scroll-behavior`,
9045
+ * 滚动停下(`SETTLE_MS` 里没有再滚)就还原成原来的内联值。返回的函数提前还原。
9046
+ *
9047
+ * 滚动本身不归它管——跳过去的仍然是浏览器(锚点跳转)。
9048
+ */
9049
+ function smoothOnce(scrollers) {
9050
+ const before = scrollers.map((scroller) => scroller.style.getPropertyValue("scroll-behavior"));
9051
+ for (const scroller of scrollers) scroller.style.setProperty("scroll-behavior", "smooth");
9052
+ const sources = scrollers.map((scroller) => scroller === document.documentElement ? window : scroller);
9053
+ let timer;
9054
+ const restore = () => {
9055
+ clearTimeout(timer);
9056
+ for (const source of sources) source.removeEventListener("scroll", wait);
9057
+ scrollers.forEach((scroller, index) => {
9058
+ scroller.style.setProperty("scroll-behavior", before[index]);
9059
+ dropEmptyStyle(scroller);
9060
+ });
9061
+ };
9062
+ function wait() {
9063
+ clearTimeout(timer);
9064
+ timer = setTimeout(restore, SETTLE_MS);
9065
+ }
9066
+ for (const source of sources) source.addEventListener("scroll", wait, { passive: true });
9067
+ wait();
9068
+ return restore;
9069
+ }
9070
+ /**
8260
9071
  * 页内目录:这一页有哪几节、现在看到了哪一节。里面放 `TocItem`。
8261
9072
  *
8262
- * 位置由使用方给,通常是 `className="sticky top-6"`。点一项就是点一个锚点链接:
8263
- * 滚不滚、平不平滑是浏览器和页面 CSS 的事,它不接管滚动。
9073
+ * 位置由使用方给,通常是 `className="sticky top-6"`。点一项仍然是点一个锚点链接——
9074
+ * 地址、后退、`scroll-mt` 都是浏览器原生的那一套;它只让这一次跳转滚得平滑
9075
+ * (系统开了"减少动态效果"时照旧直接跳)。
8264
9076
  */
8265
9077
  function Toc({ title, offset = 0, target, onActiveChange, "aria-label": ariaLabel, className, ref, children, ...props }) {
8266
9078
  const navRef = useRef(null);
9079
+ const listRef = useIndicator("[aria-current]");
8267
9080
  const titleId = useId();
8268
9081
  const count = Children.count(children);
8269
9082
  const spied = useScrollPosition(target, useCallback(({ scrolled, room }) => {
@@ -8312,6 +9125,17 @@ function Toc({ title, offset = 0, target, onActiveChange, "aria-label": ariaLabe
8312
9125
  };
8313
9126
  }, [pinned, target]);
8314
9127
  const active = pinned ?? spied;
9128
+ const restoreScroll = useRef(null);
9129
+ useEffect(() => () => restoreScroll.current?.(), []);
9130
+ const pick = useCallback((section) => {
9131
+ setPinned(section);
9132
+ restoreScroll.current?.();
9133
+ restoreScroll.current = null;
9134
+ if (!document.getElementById(section)) return;
9135
+ if (typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
9136
+ const container = target?.current;
9137
+ restoreScroll.current = smoothOnce(container ? [container, document.documentElement] : [document.documentElement]);
9138
+ }, [target]);
8315
9139
  const report = useRef(onActiveChange);
8316
9140
  report.current = onActiveChange;
8317
9141
  const reported = useRef(null);
@@ -8333,8 +9157,8 @@ function Toc({ title, offset = 0, target, onActiveChange, "aria-label": ariaLabe
8333
9157
  }, [active]);
8334
9158
  const context = useMemo(() => ({
8335
9159
  active,
8336
- pick: setPinned
8337
- }), [active]);
9160
+ pick
9161
+ }), [active, pick]);
8338
9162
  const hasTitle = title !== void 0 && title !== null && title !== false;
8339
9163
  return /* @__PURE__ */ jsxs("nav", {
8340
9164
  ...props,
@@ -8347,7 +9171,8 @@ function Toc({ title, offset = 0, target, onActiveChange, "aria-label": ariaLabe
8347
9171
  className: "mb-2 pl-4 font-tech text-xs text-ink-secondary",
8348
9172
  children: title
8349
9173
  }), /* @__PURE__ */ jsx("ul", {
8350
- className: "relative before:absolute before:inset-y-0 before:left-0 before:w-px before:bg-line before:content-['']",
9174
+ ref: listRef,
9175
+ className: cn("before:absolute before:inset-y-0 before:left-0 before:-z-1 before:w-px before:bg-line before:content-['']", indicator, "after:top-1 after:h-[calc(var(--indicator-h)-0.5rem)] after:w-[3px] after:translate-y-(--indicator-y) after:bg-ink", "forced-colors:after:bg-[Highlight]"),
8351
9176
  children: /* @__PURE__ */ jsx(TocContext, {
8352
9177
  value: context,
8353
9178
  children
@@ -8385,7 +9210,7 @@ function TocItem({ href, section = sectionOf(href), level = 1, render, onClick,
8385
9210
  onClick?.(event);
8386
9211
  if (!event.defaultPrevented && section !== void 0) pick(section);
8387
9212
  },
8388
- className: cn("relative block py-2 pr-2 leading-snug wrap-anywhere", "transition-colors duration-(--duration-fast) ease-standard", focusRingInset, levelPadding[level], current ? "text-ink before:absolute before:inset-y-1 before:left-0 before:w-[3px] before:bg-ink before:content-['']" : "text-ink-secondary hover:text-ink"),
9213
+ className: cn("relative block py-2 pr-2 leading-snug wrap-anywhere", "transition-colors duration-(--duration-fast) ease-standard", focusRingInset, levelPadding[level], current ? "text-ink before:absolute before:inset-y-1 before:left-0 before:w-[3px] before:bg-ink before:content-[''] in-data-indicator:before:hidden forced-colors:before:bg-[Highlight]" : "text-ink-secondary hover:text-ink"),
8389
9214
  children
8390
9215
  })
8391
9216
  });
@@ -8400,6 +9225,10 @@ const barVariant = {
8400
9225
  sunken: "bg-surface-sunken p-0.5",
8401
9226
  outline: "border border-line-strong bg-surface p-px"
8402
9227
  };
9228
+ const barForced = {
9229
+ sunken: "forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[ButtonText] forced-colors:data-disabled:outline-[GrayText]",
9230
+ outline: "forced-colors:border-[ButtonText] forced-colors:data-disabled:border-[GrayText]"
9231
+ };
8403
9232
  /**
8404
9233
  * 工具栏:一排作用于同一个对象的小工具,收在一条带子里。整条只占一个 Tab 停靠点,
8405
9234
  * 方向键在各项之间走。里面放 `ToolbarButton`、`ToolbarToggle`、`ToolbarToggleGroup`、
@@ -8432,7 +9261,7 @@ function Toolbar({ size = "md", variant = "sunken", orientation = "horizontal",
8432
9261
  onKeyDown: handleKeyDown,
8433
9262
  "data-size": size,
8434
9263
  "data-variant": variant,
8435
- className: cn("inline-flex max-w-full gap-0.5 text-ink", orientation === "vertical" ? "flex-col" : "flex-wrap items-center", barVariant[variant], className)
9264
+ className: cn("inline-flex max-w-full gap-0.5 text-ink", orientation === "vertical" ? "flex-col" : "flex-wrap items-center", barVariant[variant], barForced[variant], className)
8436
9265
  })
8437
9266
  });
8438
9267
  }
@@ -8457,6 +9286,10 @@ const itemBase = [
8457
9286
  "data-popup-open:bg-surface-inverse data-popup-open:text-ink-inverse",
8458
9287
  "data-disabled:cursor-not-allowed data-disabled:bg-transparent data-disabled:text-ink-disabled",
8459
9288
  "data-disabled:data-pressed:bg-disabled data-disabled:data-pressed:text-on-disabled",
9289
+ "forced-colors:data-pressed:forced-color-adjust-none forced-colors:data-pressed:outline-[Highlight]",
9290
+ "forced-colors:data-pressed:bg-[Highlight] forced-colors:data-pressed:text-[HighlightText]",
9291
+ "forced-colors:data-disabled:data-pressed:bg-[GrayText] forced-colors:data-disabled:data-pressed:text-[Canvas]",
9292
+ "forced-colors:data-disabled:text-[GrayText]",
8460
9293
  "focus-visible:z-1",
8461
9294
  focusRing
8462
9295
  ].join(" ");
@@ -8464,7 +9297,9 @@ const itemIcon = [
8464
9297
  "flex shrink-0",
8465
9298
  "group-data-pressed/item:text-accent-ink-inverse",
8466
9299
  "group-data-popup-open/item:text-accent-ink-inverse",
8467
- "group-data-disabled/item:group-data-pressed/item:text-on-disabled"
9300
+ "group-data-disabled/item:group-data-pressed/item:text-on-disabled",
9301
+ "forced-colors:group-data-pressed/item:text-[HighlightText]",
9302
+ "forced-colors:group-data-disabled/item:group-data-pressed/item:text-[Canvas]"
8468
9303
  ].join(" ");
8469
9304
  function useItemClass(iconOnly, className) {
8470
9305
  const { size } = useContext(ToolbarContext);
@@ -8555,7 +9390,7 @@ function ToolbarGroup({ className, ...props }) {
8555
9390
  function ToolbarSeparator({ className, ...props }) {
8556
9391
  return /* @__PURE__ */ jsx(Toolbar$1.Separator, {
8557
9392
  ...props,
8558
- className: cn("shrink-0 self-center bg-line-strong", "data-[orientation=vertical]:mx-1 data-[orientation=vertical]:h-4 data-[orientation=vertical]:w-px", "data-[orientation=horizontal]:my-1 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-4", className)
9393
+ className: cn("shrink-0 self-center bg-line-strong", "forced-colors:bg-[ButtonText] forced-colors:in-data-disabled:bg-[GrayText]", "data-[orientation=vertical]:mx-1 data-[orientation=vertical]:h-4 data-[orientation=vertical]:w-px", "data-[orientation=horizontal]:my-1 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-4", className)
8559
9394
  });
8560
9395
  }
8561
9396
  //#endregion
@@ -8671,6 +9506,7 @@ function TopBar({ brand, tools, action, menu, className, children, ...props }) {
8671
9506
  //#region src/hooks/useTheme.ts
8672
9507
  const STORAGE_KEY = "ef-theme";
8673
9508
  const DARK_QUERY = "(prefers-color-scheme: dark)";
9509
+ const REDUCE_QUERY = "(prefers-reduced-motion: reduce)";
8674
9510
  const serverSnapshot = {
8675
9511
  theme: "light",
8676
9512
  resolvedTheme: "light"
@@ -8678,6 +9514,8 @@ const serverSnapshot = {
8678
9514
  const listeners = /* @__PURE__ */ new Set();
8679
9515
  let snapshot = null;
8680
9516
  let stopWatchingSystem = null;
9517
+ /** 正在走的那一次切换(视图过渡):它走完才撤掉 <html> 上的标记 */
9518
+ let switching = null;
8681
9519
  function isPreference(value) {
8682
9520
  return value === "light" || value === "dark" || value === "system";
8683
9521
  }
@@ -8703,13 +9541,39 @@ function getSnapshot() {
8703
9541
  }
8704
9542
  return snapshot;
8705
9543
  }
9544
+ /**
9545
+ * 换主题。亮暗真的变了、浏览器有视图过渡、系统没开"减少动态效果"时,放在视图过渡里换:
9546
+ * 整页交叉淡化一次,而不是带换色过渡的慢慢过去、别的硬切,参差不齐。否则当场换。
9547
+ *
9548
+ * 走视图过渡的时候是晚一帧才换的——浏览器要先把旧的样子拍下来。
9549
+ * 期间 <html> 上标着 data-theme-switching:淡化的时长挂在它下面(theme.css),
9550
+ * 使用方自己的视图过渡不受影响。局部主题那几块新旧一个样,看不出变化。
9551
+ */
8706
9552
  function commit(theme) {
8707
- snapshot = {
9553
+ const next = {
8708
9554
  theme,
8709
9555
  resolvedTheme: resolve(theme)
8710
9556
  };
8711
- document.documentElement.dataset.theme = snapshot.resolvedTheme;
8712
- for (const listener of listeners) listener();
9557
+ const element = document.documentElement;
9558
+ const apply = () => {
9559
+ snapshot = next;
9560
+ element.dataset.theme = next.resolvedTheme;
9561
+ for (const listener of listeners) listener();
9562
+ };
9563
+ if (element.dataset.theme === next.resolvedTheme || typeof document.startViewTransition !== "function" || window.matchMedia?.(REDUCE_QUERY).matches) {
9564
+ apply();
9565
+ return;
9566
+ }
9567
+ element.dataset.themeSwitching = "";
9568
+ const transition = document.startViewTransition(apply);
9569
+ switching = transition;
9570
+ const done = () => {
9571
+ if (switching !== transition) return;
9572
+ switching = null;
9573
+ delete element.dataset.themeSwitching;
9574
+ };
9575
+ transition.finished.then(done, done);
9576
+ transition.ready.catch(() => {});
8713
9577
  }
8714
9578
  function subscribe(listener) {
8715
9579
  listeners.add(listener);
@@ -8740,7 +9604,8 @@ function setTheme(theme) {
8740
9604
  *
8741
9605
  * - `theme`:用户的选择,可以是 `"system"`;
8742
9606
  * - `resolvedTheme`:实际生效的 `"light"` 或 `"dark"`;
8743
- * - `setTheme`:切换并记到 `localStorage`。
9607
+ * - `setTheme`:切换并记到 `localStorage`。亮暗变了的时候整页交叉淡化一次
9608
+ * (浏览器支持视图过渡、系统没开"减少动态效果"时;否则直接切)。
8744
9609
  */
8745
9610
  function useTheme() {
8746
9611
  const { theme, resolvedTheme } = useSyncExternalStore(subscribe, getSnapshot, () => serverSnapshot);
@@ -8758,4 +9623,4 @@ function useTheme() {
8758
9623
  /** 三条横线:打开菜单 */
8759
9624
  const Menu = createIcon("Menu", /* @__PURE__ */ jsx("path", { d: "M3 6h18M3 12h18M3 18h18" }));
8760
9625
  //#endregion
8761
- export { Accordion, AccordionItem, Alert, ArrowCorner, ArrowLeft, ArrowRight, Avatar, AvatarSwitcher, AvatarSwitcherItem, BackToTop, Badge, BracketTitle, Breadcrumb, BreadcrumbItem, Button, ButtonGroup, Calendar, CalendarIcon, Carousel, CarouselSlide, Check, Checkbox, ChevronDown, ChevronLeft, ChevronRight, Close, Combobox, CompletionBanner, ContextMenu, CornerBrackets, Countdown, Crosshair, DashIndicator, DataRow, DataRowList, DatePicker, DateRangePicker, Dialog, DialogClose, Drawer, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuGroup, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, EmptyState, Field, FileItem, FileUpload, FilterChip, FlyoutBar, FlyoutBarItem, GhostText, Hatch, HazardStripe, HoverCard, IconButton, ImageViewer, ImageViewerItem, Input, ItemGrid, ItemSlot, Kbd, List, ListRow, Loader, Lock, Marquee, MediaCard, Menu, Minus, NavAction, NavMenu, NavMenuItem, Navigator, OtpInput, PageHeader, PageHeaderBack, Pagination, Panel, PanelBody, PanelHeader, PanelRow, PanelRows, PlayButton, PlayMark, Plus, Popover, PopoverClose, Progress, ProgressRing, Radio, RadioGroup, RecIndicator, RegistrationStrip, ResourceChip, Schedule, ScheduleItem, ScheduleTrack, ScrollArea, ScrollHint, SectionTitle, Segment, SegmentedControl, Select, SelectGroup, SelectItem, SelectSeparator, SideRail, SideRailGroup, SideRailItem, SideRailSub, SideRailSubItem, Skeleton, Slider, Sparkline, Spinner, Stat, StatusDanger, StatusInfo, StatusSuccess, StatusWarning, Step, Stepper, Steps, Switch, Tab, TabList, TabPanel, Table, TableBody, TableCell, TableExpander, TableHead, TableHeaderCell, TableRow, Tabs, Tag, TagInput, TagPair, Term, Textarea, Texture, TickRing, Timeline, TimelineItem, ToastProvider, Toc, TocItem, Toolbar, ToolbarButton, ToolbarGroup, ToolbarSeparator, ToolbarToggle, ToolbarToggleGroup, Tooltip, TooltipProvider, TopBar, TriangleRight, Upload, Viewfinder, cn, createIcon, useControllableState, useFieldContext, useFieldControl, useInView, usePortalScope, useTheme, useToast };
9626
+ export { Accordion, AccordionItem, Alert, ArrowCorner, ArrowLeft, ArrowRight, Avatar, AvatarSwitcher, AvatarSwitcherItem, BackToTop, Badge, BracketTitle, Breadcrumb, BreadcrumbItem, Button, ButtonGroup, Calendar, CalendarIcon, Carousel, CarouselSlide, Check, Checkbox, ChevronDown, ChevronLeft, ChevronRight, Close, Combobox, CompletionBanner, ContextMenu, CornerBrackets, Countdown, Crosshair, DashIndicator, DataRow, DataRowList, DatePicker, DateRangePicker, Dialog, DialogClose, Drawer, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuGroup, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, EmptyState, Field, FileItem, FileUpload, FilterChip, FlyoutBar, FlyoutBarItem, GhostText, Hatch, HazardStripe, HoverCard, IconButton, ImageViewer, ImageViewerItem, Input, ItemGrid, ItemSlot, Kbd, List, ListRow, Loader, Lock, Marquee, MediaCard, Menu, Minus, NavAction, NavMenu, NavMenuItem, Navigator, OtpInput, PageHeader, PageHeaderBack, Pagination, Panel, PanelBody, PanelHeader, PanelRow, PanelRows, PlayButton, PlayMark, Plus, Popover, PopoverClose, Progress, ProgressRing, Radio, RadioGroup, RecIndicator, RegistrationStrip, ResourceChip, RollingNumber, Schedule, ScheduleItem, ScheduleTrack, ScrollArea, ScrollHint, SectionTitle, Segment, SegmentedControl, Select, SelectGroup, SelectItem, SelectSeparator, SideRail, SideRailGroup, SideRailItem, SideRailSub, SideRailSubItem, Skeleton, Slider, Sparkline, Spinner, Stat, StatusDanger, StatusInfo, StatusSuccess, StatusWarning, Step, Stepper, Steps, Switch, Tab, TabList, TabPanel, Table, TableBody, TableCell, TableExpander, TableHead, TableHeaderCell, TableRow, Tabs, Tag, TagInput, TagPair, Term, Textarea, Texture, TickRing, Timeline, TimelineItem, ToastProvider, Toc, TocItem, Toolbar, ToolbarButton, ToolbarGroup, ToolbarSeparator, ToolbarToggle, ToolbarToggleGroup, Tooltip, TooltipProvider, TopBar, TriangleRight, Upload, Viewfinder, cn, createIcon, useControllableState, useFieldContext, useFieldControl, useInView, usePortalScope, useReducedMotion, useTheme, useToast };